@nasser-sw/fabric 7.0.0-beta1 → 7.0.1-beta10
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/0 +0 -0
- package/debug/{konva → konva-master}/CHANGELOG.md +2 -1
- package/debug/{konva → konva-master}/README.md +7 -3
- package/debug/{konva → konva-master}/package.json +1 -1
- package/debug/{konva → konva-master}/release.sh +1 -4
- package/debug/{konva → konva-master}/src/Canvas.ts +37 -0
- package/debug/{konva → konva-master}/src/shapes/Text.ts +2 -2
- package/dist/index.js +2198 -272
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.min.mjs.map +1 -1
- package/dist/index.mjs +2198 -272
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +2198 -272
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +2198 -272
- package/dist/index.node.mjs.map +1 -1
- package/dist/package.json.min.mjs +1 -1
- package/dist/package.json.mjs +1 -1
- package/dist/src/shapes/Line.d.ts +33 -86
- package/dist/src/shapes/Line.d.ts.map +1 -1
- package/dist/src/shapes/Line.min.mjs +1 -1
- package/dist/src/shapes/Line.min.mjs.map +1 -1
- package/dist/src/shapes/Line.mjs +405 -159
- package/dist/src/shapes/Line.mjs.map +1 -1
- package/dist/src/shapes/Polyline.d.ts +7 -0
- package/dist/src/shapes/Polyline.d.ts.map +1 -1
- package/dist/src/shapes/Polyline.min.mjs +1 -1
- package/dist/src/shapes/Polyline.min.mjs.map +1 -1
- package/dist/src/shapes/Polyline.mjs +48 -16
- package/dist/src/shapes/Polyline.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.d.ts +19 -0
- package/dist/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist/src/shapes/Text/Text.min.mjs +1 -1
- package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
- package/dist/src/shapes/Text/Text.mjs +302 -16
- package/dist/src/shapes/Text/Text.mjs.map +1 -1
- package/dist/src/shapes/Textbox.d.ts +56 -1
- package/dist/src/shapes/Textbox.d.ts.map +1 -1
- package/dist/src/shapes/Textbox.min.mjs +1 -1
- package/dist/src/shapes/Textbox.min.mjs.map +1 -1
- package/dist/src/shapes/Textbox.mjs +633 -11
- package/dist/src/shapes/Textbox.mjs.map +1 -1
- package/dist/src/shapes/Triangle.d.ts +27 -2
- package/dist/src/shapes/Triangle.d.ts.map +1 -1
- package/dist/src/shapes/Triangle.min.mjs +1 -1
- package/dist/src/shapes/Triangle.min.mjs.map +1 -1
- package/dist/src/shapes/Triangle.mjs +72 -12
- package/dist/src/shapes/Triangle.mjs.map +1 -1
- package/dist/src/text/examples/arabicTextExample.d.ts +60 -0
- package/dist/src/text/examples/arabicTextExample.d.ts.map +1 -0
- package/dist/src/text/measure.d.ts +9 -0
- package/dist/src/text/measure.d.ts.map +1 -1
- package/dist/src/text/measure.min.mjs +1 -1
- package/dist/src/text/measure.min.mjs.map +1 -1
- package/dist/src/text/measure.mjs +175 -4
- package/dist/src/text/measure.mjs.map +1 -1
- package/dist/src/text/overlayEditor.d.ts +8 -0
- package/dist/src/text/overlayEditor.d.ts.map +1 -1
- package/dist/src/text/overlayEditor.min.mjs +1 -1
- package/dist/src/text/overlayEditor.min.mjs.map +1 -1
- package/dist/src/text/overlayEditor.mjs +395 -56
- package/dist/src/text/overlayEditor.mjs.map +1 -1
- package/dist/src/text/scriptUtils.d.ts +142 -0
- package/dist/src/text/scriptUtils.d.ts.map +1 -0
- package/dist/src/text/scriptUtils.min.mjs +2 -0
- package/dist/src/text/scriptUtils.min.mjs.map +1 -0
- package/dist/src/text/scriptUtils.mjs +212 -0
- package/dist/src/text/scriptUtils.mjs.map +1 -0
- package/dist/src/util/misc/cornerRadius.d.ts +70 -0
- package/dist/src/util/misc/cornerRadius.d.ts.map +1 -0
- package/dist/src/util/misc/cornerRadius.min.mjs +2 -0
- package/dist/src/util/misc/cornerRadius.min.mjs.map +1 -0
- package/dist/src/util/misc/cornerRadius.mjs +181 -0
- package/dist/src/util/misc/cornerRadius.mjs.map +1 -0
- package/dist-extensions/src/shapes/CustomLine.d.ts +10 -0
- package/dist-extensions/src/shapes/CustomLine.d.ts.map +1 -0
- package/dist-extensions/src/shapes/Line.d.ts +33 -86
- package/dist-extensions/src/shapes/Line.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Polyline.d.ts +7 -0
- package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Text/Text.d.ts +19 -0
- package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Textbox.d.ts +56 -1
- package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Triangle.d.ts +27 -2
- package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
- package/dist-extensions/src/text/measure.d.ts +9 -0
- package/dist-extensions/src/text/measure.d.ts.map +1 -1
- package/dist-extensions/src/text/overlayEditor.d.ts +8 -0
- package/dist-extensions/src/text/overlayEditor.d.ts.map +1 -1
- package/dist-extensions/src/text/scriptUtils.d.ts +142 -0
- package/dist-extensions/src/text/scriptUtils.d.ts.map +1 -0
- package/dist-extensions/src/util/misc/cornerRadius.d.ts +70 -0
- package/dist-extensions/src/util/misc/cornerRadius.d.ts.map +1 -0
- package/fabric-test-editor.html +3552 -0
- package/fabric-test2.html +647 -0
- package/fabric.ts +182 -182
- package/fonts/STV Bold.ttf +0 -0
- package/fonts/STV Light.ttf +0 -0
- package/fonts/STV Regular.ttf +0 -0
- package/package.json +164 -164
- package/src/shapes/Line.ts +484 -157
- package/src/shapes/Polyline.ts +70 -29
- package/src/shapes/Text/Text.ts +317 -19
- package/src/shapes/Textbox.ts +663 -12
- package/src/shapes/Triangle.spec.ts +76 -0
- package/src/shapes/Triangle.ts +85 -15
- package/src/text/measure.ts +200 -50
- package/src/text/overlayEditor.ts +504 -94
- package/src/util/misc/cornerRadius.spec.ts +141 -0
- package/src/util/misc/cornerRadius.ts +269 -0
- /package/debug/{konva → konva-master}/LICENSE +0 -0
- /package/debug/{konva → konva-master}/gulpfile.mjs +0 -0
- /package/debug/{konva → konva-master}/resources/doc-includes/ContainerParams.txt +0 -0
- /package/debug/{konva → konva-master}/resources/doc-includes/NodeParams.txt +0 -0
- /package/debug/{konva → konva-master}/resources/doc-includes/ShapeParams.txt +0 -0
- /package/debug/{konva → konva-master}/resources/jsdoc.conf.json +0 -0
- /package/debug/{konva → konva-master}/rollup.config.mjs +0 -0
- /package/debug/{konva → konva-master}/src/Animation.ts +0 -0
- /package/debug/{konva → konva-master}/src/BezierFunctions.ts +0 -0
- /package/debug/{konva → konva-master}/src/Container.ts +0 -0
- /package/debug/{konva → konva-master}/src/Context.ts +0 -0
- /package/debug/{konva → konva-master}/src/Core.ts +0 -0
- /package/debug/{konva → konva-master}/src/DragAndDrop.ts +0 -0
- /package/debug/{konva → konva-master}/src/Factory.ts +0 -0
- /package/debug/{konva → konva-master}/src/FastLayer.ts +0 -0
- /package/debug/{konva → konva-master}/src/Global.ts +0 -0
- /package/debug/{konva → konva-master}/src/Group.ts +0 -0
- /package/debug/{konva → konva-master}/src/Layer.ts +0 -0
- /package/debug/{konva → konva-master}/src/Node.ts +0 -0
- /package/debug/{konva → konva-master}/src/PointerEvents.ts +0 -0
- /package/debug/{konva → konva-master}/src/Shape.ts +0 -0
- /package/debug/{konva → konva-master}/src/Stage.ts +0 -0
- /package/debug/{konva → konva-master}/src/Tween.ts +0 -0
- /package/debug/{konva → konva-master}/src/Util.ts +0 -0
- /package/debug/{konva → konva-master}/src/Validators.ts +0 -0
- /package/debug/{konva → konva-master}/src/_CoreInternals.ts +0 -0
- /package/debug/{konva → konva-master}/src/_FullInternals.ts +0 -0
- /package/debug/{konva → konva-master}/src/canvas-backend.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Blur.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Brighten.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Brightness.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Contrast.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Emboss.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Enhance.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Grayscale.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/HSL.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/HSV.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Invert.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Kaleidoscope.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Mask.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Noise.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Pixelate.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Posterize.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/RGB.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/RGBA.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Sepia.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Solarize.ts +0 -0
- /package/debug/{konva → konva-master}/src/filters/Threshold.ts +0 -0
- /package/debug/{konva → konva-master}/src/index.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Arc.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Arrow.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Circle.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Ellipse.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Image.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Label.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Line.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Path.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Rect.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/RegularPolygon.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Ring.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Sprite.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Star.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/TextPath.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Transformer.ts +0 -0
- /package/debug/{konva → konva-master}/src/shapes/Wedge.ts +0 -0
- /package/debug/{konva → konva-master}/src/skia-backend.ts +0 -0
- /package/debug/{konva → konva-master}/src/types.ts +0 -0
- /package/debug/{konva → konva-master}/tsconfig.json +0 -0
- /package/debug/{konva → konva-master}/tsconfig.test.json +0 -0
package/dist/index.js
CHANGED
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
}
|
|
361
361
|
const cache = new Cache();
|
|
362
362
|
|
|
363
|
-
var version = "7.0.
|
|
363
|
+
var version = "7.0.1-beta9";
|
|
364
364
|
|
|
365
365
|
// use this syntax so babel plugin see this import here
|
|
366
366
|
const VERSION = version;
|
|
@@ -17577,33 +17577,30 @@
|
|
|
17577
17577
|
}
|
|
17578
17578
|
}
|
|
17579
17579
|
|
|
17580
|
-
// @TODO this code is terrible and Line should be a special case of polyline.
|
|
17581
|
-
|
|
17582
17580
|
const coordProps = ['x1', 'x2', 'y1', 'y2'];
|
|
17583
|
-
/**
|
|
17584
|
-
* A Class to draw a line
|
|
17585
|
-
* A bunch of methods will be added to Polyline to handle the line case
|
|
17586
|
-
* The line class is very strange to work with, is all special, it hardly aligns
|
|
17587
|
-
* to what a developer want everytime there is an angle
|
|
17588
|
-
* @deprecated
|
|
17589
|
-
*/
|
|
17590
17581
|
class Line extends FabricObject {
|
|
17591
|
-
/**
|
|
17592
|
-
* Constructor
|
|
17593
|
-
* @param {Array} [points] Array of points
|
|
17594
|
-
* @param {Object} [options] Options object
|
|
17595
|
-
* @return {Line} thisArg
|
|
17596
|
-
*/
|
|
17597
17582
|
constructor() {
|
|
17598
|
-
let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0,
|
|
17583
|
+
let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0, 100, 0];
|
|
17599
17584
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17600
17585
|
super();
|
|
17601
|
-
|
|
17586
|
+
_defineProperty(this, "hitStrokeWidth", 'auto');
|
|
17587
|
+
_defineProperty(this, "_updatingEndpoints", false);
|
|
17588
|
+
_defineProperty(this, "_useEndpointCoords", true);
|
|
17589
|
+
_defineProperty(this, "_exportingSVG", false);
|
|
17602
17590
|
this.setOptions(options);
|
|
17603
17591
|
this.x1 = x1;
|
|
17604
17592
|
this.x2 = x2;
|
|
17605
17593
|
this.y1 = y1;
|
|
17606
17594
|
this.y2 = y2;
|
|
17595
|
+
if (options.hitStrokeWidth !== undefined) {
|
|
17596
|
+
this.hitStrokeWidth = options.hitStrokeWidth;
|
|
17597
|
+
}
|
|
17598
|
+
this.hasBorders = false;
|
|
17599
|
+
this.hasControls = true;
|
|
17600
|
+
this.selectable = true;
|
|
17601
|
+
this.hoverCursor = 'move';
|
|
17602
|
+
this.perPixelTargetFind = false;
|
|
17603
|
+
this.strokeLineCap = 'butt';
|
|
17607
17604
|
this._setWidthHeight();
|
|
17608
17605
|
const {
|
|
17609
17606
|
left,
|
|
@@ -17611,129 +17608,384 @@
|
|
|
17611
17608
|
} = options;
|
|
17612
17609
|
typeof left === 'number' && this.set(LEFT, left);
|
|
17613
17610
|
typeof top === 'number' && this.set(TOP, top);
|
|
17611
|
+
this._setupLineControls();
|
|
17612
|
+
}
|
|
17613
|
+
_setupLineControls() {
|
|
17614
|
+
this.controls = {
|
|
17615
|
+
p1: new Control({
|
|
17616
|
+
x: 0,
|
|
17617
|
+
y: 0,
|
|
17618
|
+
cursorStyle: 'move',
|
|
17619
|
+
actionHandler: this._endpointActionHandler.bind(this),
|
|
17620
|
+
positionHandler: this._p1PositionHandler.bind(this),
|
|
17621
|
+
render: this._renderEndpointControl.bind(this),
|
|
17622
|
+
sizeX: 12,
|
|
17623
|
+
sizeY: 12
|
|
17624
|
+
}),
|
|
17625
|
+
p2: new Control({
|
|
17626
|
+
x: 0,
|
|
17627
|
+
y: 0,
|
|
17628
|
+
cursorStyle: 'move',
|
|
17629
|
+
actionHandler: this._endpointActionHandler.bind(this),
|
|
17630
|
+
positionHandler: this._p2PositionHandler.bind(this),
|
|
17631
|
+
render: this._renderEndpointControl.bind(this),
|
|
17632
|
+
sizeX: 12,
|
|
17633
|
+
sizeY: 12
|
|
17634
|
+
})
|
|
17635
|
+
};
|
|
17636
|
+
}
|
|
17637
|
+
_p1PositionHandler() {
|
|
17638
|
+
return new Point(this.x1, this.y1).transform(this.getViewportTransform());
|
|
17614
17639
|
}
|
|
17640
|
+
_p2PositionHandler() {
|
|
17641
|
+
return new Point(this.x2, this.y2).transform(this.getViewportTransform());
|
|
17642
|
+
}
|
|
17643
|
+
_renderEndpointControl(ctx, left, top) {
|
|
17644
|
+
const size = 12;
|
|
17645
|
+
ctx.save();
|
|
17646
|
+
ctx.fillStyle = '#007bff';
|
|
17647
|
+
ctx.strokeStyle = '#ffffff';
|
|
17648
|
+
ctx.lineWidth = 2;
|
|
17649
|
+
ctx.beginPath();
|
|
17650
|
+
ctx.arc(left, top, size / 2, 0, 2 * Math.PI);
|
|
17651
|
+
ctx.fill();
|
|
17652
|
+
ctx.stroke();
|
|
17653
|
+
ctx.restore();
|
|
17654
|
+
}
|
|
17655
|
+
drawBorders(ctx) {
|
|
17656
|
+
let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17657
|
+
if (this._useEndpointCoords) {
|
|
17658
|
+
this._drawLineBorders(ctx, styleOverride);
|
|
17659
|
+
return this;
|
|
17660
|
+
}
|
|
17661
|
+
return super.drawBorders(ctx, styleOverride, {});
|
|
17662
|
+
}
|
|
17663
|
+
_drawLineBorders(ctx) {
|
|
17664
|
+
var _this$canvas;
|
|
17665
|
+
let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17666
|
+
const vpt = ((_this$canvas = this.canvas) === null || _this$canvas === void 0 ? void 0 : _this$canvas.viewportTransform) || [1, 0, 0, 1, 0, 0];
|
|
17667
|
+
ctx.save();
|
|
17668
|
+
ctx.setTransform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
|
|
17669
|
+
ctx.strokeStyle = styleOverride.borderColor || this.borderColor || 'rgba(100, 200, 200, 0.5)';
|
|
17670
|
+
ctx.lineWidth = (this.strokeWidth || 1) + 5;
|
|
17671
|
+
ctx.lineCap = this.strokeLineCap || 'butt';
|
|
17672
|
+
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
|
|
17673
|
+
ctx.beginPath();
|
|
17674
|
+
ctx.moveTo(this.x1, this.y1);
|
|
17675
|
+
ctx.lineTo(this.x2, this.y2);
|
|
17676
|
+
ctx.stroke();
|
|
17677
|
+
ctx.restore();
|
|
17678
|
+
}
|
|
17679
|
+
_renderControls(ctx) {
|
|
17680
|
+
let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17681
|
+
ctx.save();
|
|
17682
|
+
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
|
|
17683
|
+
this.drawControls(ctx, styleOverride);
|
|
17684
|
+
ctx.restore();
|
|
17685
|
+
}
|
|
17686
|
+
getBoundingRect() {
|
|
17687
|
+
if (this._useEndpointCoords) {
|
|
17688
|
+
const {
|
|
17689
|
+
x1,
|
|
17690
|
+
y1,
|
|
17691
|
+
x2,
|
|
17692
|
+
y2
|
|
17693
|
+
} = this;
|
|
17694
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
|
|
17695
|
+
const padding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
|
|
17696
|
+
return {
|
|
17697
|
+
left: Math.min(x1, x2) - padding,
|
|
17698
|
+
top: Math.min(y1, y2) - padding,
|
|
17699
|
+
width: Math.abs(x2 - x1) + padding * 2 || padding * 2,
|
|
17700
|
+
height: Math.abs(y2 - y1) + padding * 2 || padding * 2
|
|
17701
|
+
};
|
|
17702
|
+
}
|
|
17703
|
+
return super.getBoundingRect();
|
|
17704
|
+
}
|
|
17705
|
+
setCoords() {
|
|
17706
|
+
if (this._useEndpointCoords) {
|
|
17707
|
+
// Set width and height for hit detection and bounding box
|
|
17708
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
|
|
17709
|
+
const hitPadding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
|
|
17710
|
+
this.width = Math.abs(this.x2 - this.x1) + hitPadding * 2;
|
|
17711
|
+
this.height = Math.abs(this.y2 - this.y1) + hitPadding * 2;
|
|
17615
17712
|
|
|
17616
|
-
|
|
17617
|
-
|
|
17618
|
-
|
|
17619
|
-
|
|
17620
|
-
|
|
17621
|
-
|
|
17622
|
-
|
|
17623
|
-
|
|
17624
|
-
x2,
|
|
17625
|
-
y2
|
|
17626
|
-
} = this;
|
|
17627
|
-
this.width = Math.abs(x2 - x1);
|
|
17628
|
-
this.height = Math.abs(y2 - y1);
|
|
17629
|
-
const {
|
|
17630
|
-
left,
|
|
17631
|
-
top,
|
|
17632
|
-
width,
|
|
17633
|
-
height
|
|
17634
|
-
} = makeBoundingBoxFromPoints([{
|
|
17635
|
-
x: x1,
|
|
17636
|
-
y: y1
|
|
17637
|
-
}, {
|
|
17638
|
-
x: x2,
|
|
17639
|
-
y: y2
|
|
17640
|
-
}]);
|
|
17641
|
-
const position = new Point(left + width / 2, top + height / 2);
|
|
17642
|
-
this.setPositionByOrigin(position, CENTER, CENTER);
|
|
17713
|
+
// Only update left/top if they haven't been explicitly set (e.g., during loading)
|
|
17714
|
+
if (this.left === 0 && this.top === 0) {
|
|
17715
|
+
const center = this._findCenterFromElement();
|
|
17716
|
+
this.left = center.x;
|
|
17717
|
+
this.top = center.y;
|
|
17718
|
+
}
|
|
17719
|
+
}
|
|
17720
|
+
super.setCoords();
|
|
17643
17721
|
}
|
|
17722
|
+
getCoords() {
|
|
17723
|
+
if (this._useEndpointCoords) {
|
|
17724
|
+
const deltaX = this.x2 - this.x1;
|
|
17725
|
+
const deltaY = this.y2 - this.y1;
|
|
17726
|
+
const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
17727
|
+
if (length === 0) {
|
|
17728
|
+
return super.getCoords();
|
|
17729
|
+
}
|
|
17730
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
|
|
17731
|
+
const halfWidth = Math.max(effectiveStrokeWidth / 2 + 2, 5);
|
|
17644
17732
|
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17733
|
+
// Unit vector perpendicular to line
|
|
17734
|
+
const perpX = -deltaY / length;
|
|
17735
|
+
const perpY = deltaX / length;
|
|
17736
|
+
|
|
17737
|
+
// Four corners of oriented rectangle
|
|
17738
|
+
return [new Point(this.x1 + perpX * halfWidth, this.y1 + perpY * halfWidth), new Point(this.x2 + perpX * halfWidth, this.y2 + perpY * halfWidth), new Point(this.x2 - perpX * halfWidth, this.y2 - perpY * halfWidth), new Point(this.x1 - perpX * halfWidth, this.y1 - perpY * halfWidth)];
|
|
17739
|
+
}
|
|
17740
|
+
return super.getCoords();
|
|
17741
|
+
}
|
|
17742
|
+
containsPoint(point) {
|
|
17743
|
+
if (this._useEndpointCoords) {
|
|
17744
|
+
var _this$canvas2;
|
|
17745
|
+
if (((_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 ? void 0 : _this$canvas2.getActiveObject()) === this) {
|
|
17746
|
+
return super.containsPoint(point);
|
|
17747
|
+
}
|
|
17748
|
+
const distance = this._distanceToLineSegment(point.x, point.y);
|
|
17749
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth || 1;
|
|
17750
|
+
const tolerance = Math.max(effectiveStrokeWidth / 2 + 2, 5);
|
|
17751
|
+
return distance <= tolerance;
|
|
17752
|
+
}
|
|
17753
|
+
return super.containsPoint(point);
|
|
17754
|
+
}
|
|
17755
|
+
_distanceToLineSegment(px, py) {
|
|
17756
|
+
const x1 = this.x1,
|
|
17757
|
+
y1 = this.y1,
|
|
17758
|
+
x2 = this.x2,
|
|
17759
|
+
y2 = this.y2;
|
|
17760
|
+
const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
|
|
17761
|
+
if (pd2 === 0) {
|
|
17762
|
+
return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
|
|
17763
|
+
}
|
|
17764
|
+
const u = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / pd2;
|
|
17765
|
+
let closestX, closestY;
|
|
17766
|
+
if (u < 0) {
|
|
17767
|
+
closestX = x1;
|
|
17768
|
+
closestY = y1;
|
|
17769
|
+
} else if (u > 1) {
|
|
17770
|
+
closestX = x2;
|
|
17771
|
+
closestY = y2;
|
|
17772
|
+
} else {
|
|
17773
|
+
closestX = x1 + u * (x2 - x1);
|
|
17774
|
+
closestY = y1 + u * (y2 - y1);
|
|
17775
|
+
}
|
|
17776
|
+
return Math.sqrt((px - closestX) * (px - closestX) + (py - closestY) * (py - closestY));
|
|
17777
|
+
}
|
|
17778
|
+
_endpointActionHandler(eventData, transformData, x, y) {
|
|
17779
|
+
var _this$canvas4;
|
|
17780
|
+
const controlKey = transformData.corner;
|
|
17781
|
+
const pointer = new Point(x, y);
|
|
17782
|
+
let newX = pointer.x;
|
|
17783
|
+
let newY = pointer.y;
|
|
17784
|
+
if (eventData.shiftKey) {
|
|
17785
|
+
const otherControl = controlKey === 'p1' ? 'p2' : 'p1';
|
|
17786
|
+
const otherX = this[otherControl === 'p1' ? 'x1' : 'x2'];
|
|
17787
|
+
const otherY = this[otherControl === 'p1' ? 'y1' : 'y2'];
|
|
17788
|
+
const snapped = this._snapToAngle(otherX, otherY, newX, newY);
|
|
17789
|
+
newX = snapped.x;
|
|
17790
|
+
newY = snapped.y;
|
|
17791
|
+
}
|
|
17792
|
+
if (this._useEndpointCoords) {
|
|
17793
|
+
var _this$canvas3;
|
|
17794
|
+
if (controlKey === 'p1') {
|
|
17795
|
+
this.x1 = newX;
|
|
17796
|
+
this.y1 = newY;
|
|
17797
|
+
} else if (controlKey === 'p2') {
|
|
17798
|
+
this.x2 = newX;
|
|
17799
|
+
this.y2 = newY;
|
|
17800
|
+
}
|
|
17801
|
+
|
|
17802
|
+
// Update gradient coordinates if stroke is a gradient (but not during SVG export)
|
|
17803
|
+
if (this.stroke instanceof Gradient && !this._exportingSVG) {
|
|
17804
|
+
this.stroke.coords.x1 = this.x1;
|
|
17805
|
+
this.stroke.coords.y1 = this.y1;
|
|
17806
|
+
this.stroke.coords.x2 = this.x2;
|
|
17807
|
+
this.stroke.coords.y2 = this.y2;
|
|
17808
|
+
}
|
|
17809
|
+
this.dirty = true;
|
|
17810
|
+
this.setCoords();
|
|
17811
|
+
(_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
|
|
17812
|
+
return true;
|
|
17813
|
+
}
|
|
17814
|
+
|
|
17815
|
+
// Fallback for old system
|
|
17816
|
+
this._updatingEndpoints = true;
|
|
17817
|
+
if (controlKey === 'p1') {
|
|
17818
|
+
this.x1 = newX;
|
|
17819
|
+
this.y1 = newY;
|
|
17820
|
+
} else if (controlKey === 'p2') {
|
|
17821
|
+
this.x2 = newX;
|
|
17822
|
+
this.y2 = newY;
|
|
17823
|
+
}
|
|
17824
|
+
this._setWidthHeight();
|
|
17825
|
+
this.dirty = true;
|
|
17826
|
+
this._updatingEndpoints = false;
|
|
17827
|
+
(_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
|
|
17828
|
+
this.fire('modified', {
|
|
17829
|
+
transform: transformData,
|
|
17830
|
+
target: this,
|
|
17831
|
+
e: eventData
|
|
17832
|
+
});
|
|
17833
|
+
return true;
|
|
17834
|
+
}
|
|
17835
|
+
_snapToAngle(fromX, fromY, toX, toY) {
|
|
17836
|
+
const deltaX = toX - fromX;
|
|
17837
|
+
const deltaY = toY - fromY;
|
|
17838
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
17839
|
+
if (distance === 0) return {
|
|
17840
|
+
x: toX,
|
|
17841
|
+
y: toY
|
|
17842
|
+
};
|
|
17843
|
+
let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
|
|
17844
|
+
const snapIncrement = 15;
|
|
17845
|
+
const snappedAngle = Math.round(angle / snapIncrement) * snapIncrement;
|
|
17846
|
+
const snappedRadians = snappedAngle * (Math.PI / 180);
|
|
17847
|
+
return {
|
|
17848
|
+
x: fromX + Math.cos(snappedRadians) * distance,
|
|
17849
|
+
y: fromY + Math.sin(snappedRadians) * distance
|
|
17850
|
+
};
|
|
17851
|
+
}
|
|
17852
|
+
_setWidthHeight() {
|
|
17853
|
+
let skipReposition = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
|
17854
|
+
this.width = Math.abs(this.x2 - this.x1) || 1;
|
|
17855
|
+
this.height = Math.abs(this.y2 - this.y1) || 1;
|
|
17856
|
+
if (!skipReposition && !this._updatingEndpoints) {
|
|
17857
|
+
const {
|
|
17858
|
+
left,
|
|
17859
|
+
top,
|
|
17860
|
+
width,
|
|
17861
|
+
height
|
|
17862
|
+
} = makeBoundingBoxFromPoints([{
|
|
17863
|
+
x: this.x1,
|
|
17864
|
+
y: this.y1
|
|
17865
|
+
}, {
|
|
17866
|
+
x: this.x2,
|
|
17867
|
+
y: this.y2
|
|
17868
|
+
}]);
|
|
17869
|
+
this.setPositionByOrigin(new Point(left + width / 2, top + height / 2), CENTER, CENTER);
|
|
17870
|
+
}
|
|
17871
|
+
}
|
|
17650
17872
|
_set(key, value) {
|
|
17873
|
+
const oldLeft = this.left;
|
|
17874
|
+
const oldTop = this.top;
|
|
17651
17875
|
super._set(key, value);
|
|
17652
17876
|
if (coordProps.includes(key)) {
|
|
17653
|
-
// this doesn't make sense very much, since setting x1 when top or left
|
|
17654
|
-
// are already set, is just going to show a strange result since the
|
|
17655
|
-
// line will move way more than the developer expect.
|
|
17656
|
-
// in fabric5 it worked only when the line didn't have extra transformations,
|
|
17657
|
-
// in fabric6 too. With extra transform they behave bad in different ways.
|
|
17658
|
-
// This needs probably a good rework or a tutorial if you have to create a dynamic line
|
|
17659
17877
|
this._setWidthHeight();
|
|
17878
|
+
this.dirty = true;
|
|
17879
|
+
|
|
17880
|
+
// Update gradient coordinates if stroke is a gradient (but not during SVG export)
|
|
17881
|
+
if (this.stroke instanceof Gradient && !this._exportingSVG) {
|
|
17882
|
+
this.stroke.coords.x1 = this.x1;
|
|
17883
|
+
this.stroke.coords.y1 = this.y1;
|
|
17884
|
+
this.stroke.coords.x2 = this.x2;
|
|
17885
|
+
this.stroke.coords.y2 = this.y2;
|
|
17886
|
+
}
|
|
17887
|
+
}
|
|
17888
|
+
if ((key === 'left' || key === 'top') && this.canvas && !this._updatingEndpoints) {
|
|
17889
|
+
const deltaX = this.left - oldLeft;
|
|
17890
|
+
const deltaY = this.top - oldTop;
|
|
17891
|
+
if (deltaX !== 0 || deltaY !== 0) {
|
|
17892
|
+
this._updatingEndpoints = true;
|
|
17893
|
+
this.x1 += deltaX;
|
|
17894
|
+
this.y1 += deltaY;
|
|
17895
|
+
this.x2 += deltaX;
|
|
17896
|
+
this.y2 += deltaY;
|
|
17897
|
+
|
|
17898
|
+
// Update gradient coordinates if stroke is a gradient
|
|
17899
|
+
if (this.stroke instanceof Gradient) {
|
|
17900
|
+
this.stroke.coords.x1 = this.x1;
|
|
17901
|
+
this.stroke.coords.y1 = this.y1;
|
|
17902
|
+
this.stroke.coords.x2 = this.x2;
|
|
17903
|
+
this.stroke.coords.y2 = this.y2;
|
|
17904
|
+
}
|
|
17905
|
+
this._updatingEndpoints = false;
|
|
17906
|
+
}
|
|
17660
17907
|
}
|
|
17661
17908
|
return this;
|
|
17662
17909
|
}
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
|
|
17666
|
-
|
|
17667
|
-
|
|
17910
|
+
render(ctx) {
|
|
17911
|
+
if (this._useEndpointCoords) {
|
|
17912
|
+
this._renderDirectly(ctx);
|
|
17913
|
+
return;
|
|
17914
|
+
}
|
|
17915
|
+
super.render(ctx);
|
|
17916
|
+
}
|
|
17917
|
+
_renderDirectly(ctx) {
|
|
17918
|
+
if (!this.visible) return;
|
|
17919
|
+
ctx.save();
|
|
17920
|
+
ctx.globalAlpha = this.opacity;
|
|
17921
|
+
ctx.lineWidth = this.strokeWidth;
|
|
17922
|
+
ctx.lineCap = this.strokeLineCap || 'butt';
|
|
17923
|
+
ctx.beginPath();
|
|
17924
|
+
ctx.moveTo(this.x1, this.y1);
|
|
17925
|
+
ctx.lineTo(this.x2, this.y2);
|
|
17926
|
+
const origStrokeStyle = ctx.strokeStyle;
|
|
17927
|
+
if (isFiller(this.stroke)) {
|
|
17928
|
+
ctx.strokeStyle = this.stroke.toLive(ctx);
|
|
17929
|
+
} else {
|
|
17930
|
+
var _this$stroke;
|
|
17931
|
+
ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
|
|
17932
|
+
}
|
|
17933
|
+
ctx.stroke();
|
|
17934
|
+
ctx.strokeStyle = origStrokeStyle;
|
|
17935
|
+
ctx.restore();
|
|
17936
|
+
}
|
|
17668
17937
|
_render(ctx) {
|
|
17938
|
+
if (this._useEndpointCoords) return;
|
|
17669
17939
|
ctx.beginPath();
|
|
17670
17940
|
const p = this.calcLinePoints();
|
|
17671
17941
|
ctx.moveTo(p.x1, p.y1);
|
|
17672
17942
|
ctx.lineTo(p.x2, p.y2);
|
|
17673
17943
|
ctx.lineWidth = this.strokeWidth;
|
|
17674
|
-
|
|
17675
|
-
// TODO: test this
|
|
17676
|
-
// make sure setting "fill" changes color of a line
|
|
17677
|
-
// (by copying fillStyle to strokeStyle, since line is stroked, not filled)
|
|
17678
17944
|
const origStrokeStyle = ctx.strokeStyle;
|
|
17679
17945
|
if (isFiller(this.stroke)) {
|
|
17680
17946
|
ctx.strokeStyle = this.stroke.toLive(ctx);
|
|
17681
|
-
} else {
|
|
17682
|
-
var _this$stroke;
|
|
17683
|
-
ctx.strokeStyle = (_this$stroke = this.stroke) !== null && _this$stroke !== void 0 ? _this$stroke : ctx.fillStyle;
|
|
17684
17947
|
}
|
|
17685
17948
|
this.stroke && this._renderStroke(ctx);
|
|
17686
17949
|
ctx.strokeStyle = origStrokeStyle;
|
|
17687
17950
|
}
|
|
17688
|
-
|
|
17689
|
-
/**
|
|
17690
|
-
* This function is an helper for svg import. it returns the center of the object in the svg
|
|
17691
|
-
* untransformed coordinates
|
|
17692
|
-
* @private
|
|
17693
|
-
* @return {Point} center point from element coordinates
|
|
17694
|
-
*/
|
|
17695
17951
|
_findCenterFromElement() {
|
|
17696
17952
|
return new Point((this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
|
|
17697
17953
|
}
|
|
17698
|
-
|
|
17699
|
-
/**
|
|
17700
|
-
* Returns object representation of an instance
|
|
17701
|
-
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
17702
|
-
* @return {Object} object representation of an instance
|
|
17703
|
-
*/
|
|
17704
17954
|
toObject() {
|
|
17705
17955
|
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
17956
|
+
if (this._useEndpointCoords) {
|
|
17957
|
+
return {
|
|
17958
|
+
...super.toObject(propertiesToInclude),
|
|
17959
|
+
x1: this.x1,
|
|
17960
|
+
y1: this.y1,
|
|
17961
|
+
x2: this.x2,
|
|
17962
|
+
y2: this.y2
|
|
17963
|
+
};
|
|
17964
|
+
}
|
|
17706
17965
|
return {
|
|
17707
17966
|
...super.toObject(propertiesToInclude),
|
|
17708
17967
|
...this.calcLinePoints()
|
|
17709
17968
|
};
|
|
17710
17969
|
}
|
|
17711
|
-
|
|
17712
|
-
/*
|
|
17713
|
-
* Calculate object dimensions from its properties
|
|
17714
|
-
* @private
|
|
17715
|
-
*/
|
|
17716
17970
|
_getNonTransformedDimensions() {
|
|
17717
17971
|
const dim = super._getNonTransformedDimensions();
|
|
17718
|
-
if (this.strokeLineCap === '
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
}
|
|
17722
|
-
if (this.height === 0) {
|
|
17723
|
-
dim.x -= this.strokeWidth;
|
|
17724
|
-
}
|
|
17972
|
+
if (this.strokeLineCap === 'round') {
|
|
17973
|
+
dim.x += this.strokeWidth;
|
|
17974
|
+
dim.y += this.strokeWidth;
|
|
17725
17975
|
}
|
|
17726
17976
|
return dim;
|
|
17727
17977
|
}
|
|
17728
|
-
|
|
17729
|
-
/**
|
|
17730
|
-
* Recalculates line points given width and height
|
|
17731
|
-
* Those points are simply placed around the center,
|
|
17732
|
-
* This is not useful outside internal render functions and svg output
|
|
17733
|
-
* Is not meant to be for the developer.
|
|
17734
|
-
* @private
|
|
17735
|
-
*/
|
|
17736
17978
|
calcLinePoints() {
|
|
17979
|
+
if (this._updatingEndpoints) {
|
|
17980
|
+
const centerX = (this.x1 + this.x2) / 2;
|
|
17981
|
+
const centerY = (this.y1 + this.y2) / 2;
|
|
17982
|
+
return {
|
|
17983
|
+
x1: this.x1 - centerX,
|
|
17984
|
+
y1: this.y1 - centerY,
|
|
17985
|
+
x2: this.x2 - centerX,
|
|
17986
|
+
y2: this.y2 - centerY
|
|
17987
|
+
};
|
|
17988
|
+
}
|
|
17737
17989
|
const {
|
|
17738
17990
|
x1: _x1,
|
|
17739
17991
|
x2: _x2,
|
|
@@ -17742,48 +17994,64 @@
|
|
|
17742
17994
|
width,
|
|
17743
17995
|
height
|
|
17744
17996
|
} = this;
|
|
17745
|
-
const xMult = _x1 <= _x2 ? -1 : 1
|
|
17746
|
-
|
|
17747
|
-
x1 = xMult * width / 2,
|
|
17748
|
-
y1 = yMult * height / 2,
|
|
17749
|
-
x2 = xMult * -width / 2,
|
|
17750
|
-
y2 = yMult * -height / 2;
|
|
17997
|
+
const xMult = _x1 <= _x2 ? -1 : 1;
|
|
17998
|
+
const yMult = _y1 <= _y2 ? -1 : 1;
|
|
17751
17999
|
return {
|
|
17752
|
-
x1,
|
|
17753
|
-
|
|
17754
|
-
|
|
17755
|
-
y2
|
|
18000
|
+
x1: xMult * width / 2,
|
|
18001
|
+
y1: yMult * height / 2,
|
|
18002
|
+
x2: xMult * -width / 2,
|
|
18003
|
+
y2: yMult * -height / 2
|
|
17756
18004
|
};
|
|
17757
18005
|
}
|
|
17758
|
-
|
|
17759
|
-
/* _FROM_SVG_START_ */
|
|
17760
|
-
|
|
17761
|
-
/**
|
|
17762
|
-
* Returns svg representation of an instance
|
|
17763
|
-
* @return {Array} an array of strings with the specific svg representation
|
|
17764
|
-
* of the instance
|
|
17765
|
-
*/
|
|
17766
18006
|
_toSVG() {
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
18007
|
+
if (this._useEndpointCoords) {
|
|
18008
|
+
// Use absolute coordinates to bypass all Fabric.js transforms
|
|
18009
|
+
// Handle gradients manually for proper SVG export
|
|
18010
|
+
let strokeAttr = '';
|
|
18011
|
+
if (this.stroke instanceof Gradient) {
|
|
18012
|
+
// Let Fabric.js handle gradient definition, but we'll use the reference
|
|
18013
|
+
strokeAttr = `stroke="url(#${this.stroke.id})"`;
|
|
18014
|
+
} else {
|
|
18015
|
+
strokeAttr = `stroke="${this.stroke || 'none'}"`;
|
|
18016
|
+
}
|
|
18017
|
+
return [`<line ${strokeAttr} stroke-width="${this.strokeWidth}" stroke-linecap="${this.strokeLineCap}" `, `stroke-dasharray="${this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none'}" `, `stroke-dashoffset="${this.strokeDashOffset}" stroke-linejoin="${this.strokeLineJoin}" `, `stroke-miterlimit="${this.strokeMiterLimit}" fill="${this.fill || 'none'}" `, `fill-rule="${this.fillRule}" opacity="${this.opacity}" `, `x1="${this.x1}" y1="${this.y1}" x2="${this.x2}" y2="${this.y2}" />\n`];
|
|
18018
|
+
} else {
|
|
18019
|
+
// Use standard calcLinePoints for legacy mode
|
|
18020
|
+
const {
|
|
18021
|
+
x1,
|
|
18022
|
+
x2,
|
|
18023
|
+
y1,
|
|
18024
|
+
y2
|
|
18025
|
+
} = this.calcLinePoints();
|
|
18026
|
+
return ['<line ', 'COMMON_PARTS', `x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`];
|
|
18027
|
+
}
|
|
17774
18028
|
}
|
|
18029
|
+
toSVG(reviver) {
|
|
18030
|
+
if (this._useEndpointCoords) {
|
|
18031
|
+
// For endpoint coords, we need to bypass transforms but still allow gradients
|
|
18032
|
+
// Let's temporarily disable transforms during SVG generation
|
|
18033
|
+
const originalLeft = this.left;
|
|
18034
|
+
const originalTop = this.top;
|
|
17775
18035
|
|
|
17776
|
-
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
*/
|
|
18036
|
+
// Set position to center of line for gradient calculation
|
|
18037
|
+
this.left = (this.x1 + this.x2) / 2;
|
|
18038
|
+
this.top = (this.y1 + this.y2) / 2;
|
|
17780
18039
|
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
|
|
17786
|
-
|
|
18040
|
+
// Get the SVG with standard system (for gradient handling)
|
|
18041
|
+
const standardSVG = super.toSVG(reviver);
|
|
18042
|
+
|
|
18043
|
+
// Restore original position
|
|
18044
|
+
this.left = originalLeft;
|
|
18045
|
+
this.top = originalTop;
|
|
18046
|
+
|
|
18047
|
+
// Extract gradient definition and clean up the line element
|
|
18048
|
+
// Remove the transform wrapper and update coordinates
|
|
18049
|
+
const cleanSVG = standardSVG.replace(/<g transform="[^"]*"[^>]*>/g, '').replace(/<\/g>/g, '').replace(/x1="[^"]*"/g, `x1="${this.x1}"`).replace(/y1="[^"]*"/g, `y1="${this.y1}"`).replace(/x2="[^"]*"/g, `x2="${this.x2}"`).replace(/y2="[^"]*"/g, `y2="${this.y2}"`);
|
|
18050
|
+
return cleanSVG;
|
|
18051
|
+
}
|
|
18052
|
+
// Use default behavior for legacy mode
|
|
18053
|
+
return super.toSVG(reviver);
|
|
18054
|
+
}
|
|
17787
18055
|
static async fromElement(element, options, cssRules) {
|
|
17788
18056
|
const {
|
|
17789
18057
|
x1 = 0,
|
|
@@ -17794,14 +18062,6 @@
|
|
|
17794
18062
|
} = parseAttributes(element, this.ATTRIBUTE_NAMES, cssRules);
|
|
17795
18063
|
return new this([x1, y1, x2, y2], parsedAttributes);
|
|
17796
18064
|
}
|
|
17797
|
-
|
|
17798
|
-
/* _FROM_SVG_END_ */
|
|
17799
|
-
|
|
17800
|
-
/**
|
|
17801
|
-
* Returns Line instance from an object representation
|
|
17802
|
-
* @param {Object} object Object to create an instance from
|
|
17803
|
-
* @returns {Promise<Line>}
|
|
17804
|
-
*/
|
|
17805
18065
|
static fromObject(_ref) {
|
|
17806
18066
|
let {
|
|
17807
18067
|
x1,
|
|
@@ -17818,32 +18078,195 @@
|
|
|
17818
18078
|
});
|
|
17819
18079
|
}
|
|
17820
18080
|
}
|
|
18081
|
+
_defineProperty(Line, "type", 'Line');
|
|
18082
|
+
_defineProperty(Line, "cacheProperties", [...cacheProperties, ...coordProps]);
|
|
18083
|
+
_defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
|
|
18084
|
+
classRegistry.setClass(Line);
|
|
18085
|
+
classRegistry.setSVGClass(Line);
|
|
18086
|
+
|
|
17821
18087
|
/**
|
|
17822
|
-
*
|
|
17823
|
-
* @type number
|
|
18088
|
+
* Calculate the distance between two points
|
|
17824
18089
|
*/
|
|
18090
|
+
function pointDistance(p1, p2) {
|
|
18091
|
+
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
|
18092
|
+
}
|
|
18093
|
+
|
|
17825
18094
|
/**
|
|
17826
|
-
*
|
|
17827
|
-
* @type number
|
|
18095
|
+
* Normalize a vector
|
|
17828
18096
|
*/
|
|
18097
|
+
function normalizeVector(vector) {
|
|
18098
|
+
const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
|
|
18099
|
+
if (length === 0) return {
|
|
18100
|
+
x: 0,
|
|
18101
|
+
y: 0
|
|
18102
|
+
};
|
|
18103
|
+
return {
|
|
18104
|
+
x: vector.x / length,
|
|
18105
|
+
y: vector.y / length
|
|
18106
|
+
};
|
|
18107
|
+
}
|
|
18108
|
+
|
|
17829
18109
|
/**
|
|
17830
|
-
*
|
|
17831
|
-
* @type number
|
|
18110
|
+
* Get the maximum allowed radius for a corner based on adjacent edge lengths
|
|
17832
18111
|
*/
|
|
18112
|
+
function getMaxRadius(prevPoint, currentPoint, nextPoint) {
|
|
18113
|
+
const dist1 = pointDistance(prevPoint, currentPoint);
|
|
18114
|
+
const dist2 = pointDistance(currentPoint, nextPoint);
|
|
18115
|
+
return Math.min(dist1, dist2) / 2;
|
|
18116
|
+
}
|
|
18117
|
+
|
|
17833
18118
|
/**
|
|
17834
|
-
*
|
|
17835
|
-
* @type number
|
|
18119
|
+
* Calculate rounded corner data for a single corner
|
|
17836
18120
|
*/
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
18121
|
+
function calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius) {
|
|
18122
|
+
// Calculate edge vectors
|
|
18123
|
+
const edge1 = {
|
|
18124
|
+
x: currentPoint.x - prevPoint.x,
|
|
18125
|
+
y: currentPoint.y - prevPoint.y
|
|
18126
|
+
};
|
|
18127
|
+
const edge2 = {
|
|
18128
|
+
x: nextPoint.x - currentPoint.x,
|
|
18129
|
+
y: nextPoint.y - currentPoint.y
|
|
18130
|
+
};
|
|
18131
|
+
|
|
18132
|
+
// Normalize edge vectors
|
|
18133
|
+
const norm1 = normalizeVector(edge1);
|
|
18134
|
+
const norm2 = normalizeVector(edge2);
|
|
18135
|
+
|
|
18136
|
+
// Calculate the maximum allowed radius
|
|
18137
|
+
const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
|
|
18138
|
+
const actualRadius = Math.min(radius, maxRadius);
|
|
18139
|
+
|
|
18140
|
+
// Calculate start and end points of the rounded corner
|
|
18141
|
+
const startPoint = {
|
|
18142
|
+
x: currentPoint.x - norm1.x * actualRadius,
|
|
18143
|
+
y: currentPoint.y - norm1.y * actualRadius
|
|
18144
|
+
};
|
|
18145
|
+
const endPoint = {
|
|
18146
|
+
x: currentPoint.x + norm2.x * actualRadius,
|
|
18147
|
+
y: currentPoint.y + norm2.y * actualRadius
|
|
18148
|
+
};
|
|
18149
|
+
|
|
18150
|
+
// Calculate control points for bezier curve
|
|
18151
|
+
// Using the magic number kRect for optimal circular approximation
|
|
18152
|
+
const controlOffset = actualRadius * kRect;
|
|
18153
|
+
const cp1 = {
|
|
18154
|
+
x: startPoint.x + norm1.x * controlOffset,
|
|
18155
|
+
y: startPoint.y + norm1.y * controlOffset
|
|
18156
|
+
};
|
|
18157
|
+
const cp2 = {
|
|
18158
|
+
x: endPoint.x - norm2.x * controlOffset,
|
|
18159
|
+
y: endPoint.y - norm2.y * controlOffset
|
|
18160
|
+
};
|
|
18161
|
+
return {
|
|
18162
|
+
corner: currentPoint,
|
|
18163
|
+
start: startPoint,
|
|
18164
|
+
end: endPoint,
|
|
18165
|
+
cp1,
|
|
18166
|
+
cp2,
|
|
18167
|
+
actualRadius
|
|
18168
|
+
};
|
|
18169
|
+
}
|
|
18170
|
+
|
|
18171
|
+
/**
|
|
18172
|
+
* Apply corner radius to a polygon defined by points
|
|
18173
|
+
*/
|
|
18174
|
+
function applyCornerRadiusToPolygon(points, radius) {
|
|
18175
|
+
let radiusAsPercentage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
18176
|
+
if (points.length < 3) {
|
|
18177
|
+
throw new Error('Polygon must have at least 3 points');
|
|
18178
|
+
}
|
|
18179
|
+
|
|
18180
|
+
// Calculate bounding box if radius is percentage-based
|
|
18181
|
+
let actualRadius = radius;
|
|
18182
|
+
if (radiusAsPercentage) {
|
|
18183
|
+
const minX = Math.min(...points.map(p => p.x));
|
|
18184
|
+
const maxX = Math.max(...points.map(p => p.x));
|
|
18185
|
+
const minY = Math.min(...points.map(p => p.y));
|
|
18186
|
+
const maxY = Math.max(...points.map(p => p.y));
|
|
18187
|
+
const width = maxX - minX;
|
|
18188
|
+
const height = maxY - minY;
|
|
18189
|
+
const minDimension = Math.min(width, height);
|
|
18190
|
+
actualRadius = radius / 100 * minDimension;
|
|
18191
|
+
}
|
|
18192
|
+
const roundedCorners = [];
|
|
18193
|
+
for (let i = 0; i < points.length; i++) {
|
|
18194
|
+
const prevIndex = (i - 1 + points.length) % points.length;
|
|
18195
|
+
const nextIndex = (i + 1) % points.length;
|
|
18196
|
+
const prevPoint = points[prevIndex];
|
|
18197
|
+
const currentPoint = points[i];
|
|
18198
|
+
const nextPoint = points[nextIndex];
|
|
18199
|
+
const roundedCorner = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, actualRadius);
|
|
18200
|
+
roundedCorners.push(roundedCorner);
|
|
18201
|
+
}
|
|
18202
|
+
return roundedCorners;
|
|
18203
|
+
}
|
|
18204
|
+
|
|
18205
|
+
/**
|
|
18206
|
+
* Render a rounded polygon to a canvas context
|
|
18207
|
+
*/
|
|
18208
|
+
function renderRoundedPolygon(ctx, roundedCorners) {
|
|
18209
|
+
let closed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
18210
|
+
if (roundedCorners.length === 0) return;
|
|
18211
|
+
ctx.beginPath();
|
|
18212
|
+
|
|
18213
|
+
// Start at the first corner's start point
|
|
18214
|
+
const firstCorner = roundedCorners[0];
|
|
18215
|
+
ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
|
|
18216
|
+
for (let i = 0; i < roundedCorners.length; i++) {
|
|
18217
|
+
const corner = roundedCorners[i];
|
|
18218
|
+
const nextIndex = (i + 1) % roundedCorners.length;
|
|
18219
|
+
const nextCorner = roundedCorners[nextIndex];
|
|
18220
|
+
|
|
18221
|
+
// Draw the rounded corner using bezier curve
|
|
18222
|
+
ctx.bezierCurveTo(corner.cp1.x, corner.cp1.y, corner.cp2.x, corner.cp2.y, corner.end.x, corner.end.y);
|
|
18223
|
+
|
|
18224
|
+
// Draw line to next corner's start point (if not the last segment in open path)
|
|
18225
|
+
if (i < roundedCorners.length - 1 || closed) {
|
|
18226
|
+
ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
|
|
18227
|
+
}
|
|
18228
|
+
}
|
|
18229
|
+
if (closed) {
|
|
18230
|
+
ctx.closePath();
|
|
18231
|
+
}
|
|
18232
|
+
}
|
|
18233
|
+
|
|
18234
|
+
/**
|
|
18235
|
+
* Generate SVG path data for a rounded polygon
|
|
18236
|
+
*/
|
|
18237
|
+
function generateRoundedPolygonPath(roundedCorners) {
|
|
18238
|
+
let closed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
|
18239
|
+
if (roundedCorners.length === 0) return '';
|
|
18240
|
+
const pathData = [];
|
|
18241
|
+
const firstCorner = roundedCorners[0];
|
|
18242
|
+
|
|
18243
|
+
// Move to first corner's start point
|
|
18244
|
+
pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
|
|
18245
|
+
for (let i = 0; i < roundedCorners.length; i++) {
|
|
18246
|
+
const corner = roundedCorners[i];
|
|
18247
|
+
const nextIndex = (i + 1) % roundedCorners.length;
|
|
18248
|
+
const nextCorner = roundedCorners[nextIndex];
|
|
18249
|
+
|
|
18250
|
+
// Add bezier curve for the rounded corner
|
|
18251
|
+
pathData.push(`C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`);
|
|
18252
|
+
|
|
18253
|
+
// Add line to next corner's start point (if not the last segment in open path)
|
|
18254
|
+
if (i < roundedCorners.length - 1 || closed) {
|
|
18255
|
+
pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
|
|
18256
|
+
}
|
|
18257
|
+
}
|
|
18258
|
+
if (closed) {
|
|
18259
|
+
pathData.push('Z');
|
|
18260
|
+
}
|
|
18261
|
+
return pathData.join(' ');
|
|
18262
|
+
}
|
|
17842
18263
|
|
|
17843
18264
|
const triangleDefaultValues = {
|
|
17844
18265
|
width: 100,
|
|
17845
|
-
height: 100
|
|
18266
|
+
height: 100,
|
|
18267
|
+
cornerRadius: 0
|
|
17846
18268
|
};
|
|
18269
|
+
const TRIANGLE_PROPS = ['cornerRadius'];
|
|
17847
18270
|
class Triangle extends FabricObject {
|
|
17848
18271
|
static getDefaults() {
|
|
17849
18272
|
return {
|
|
@@ -17862,34 +18285,90 @@
|
|
|
17862
18285
|
this.setOptions(options);
|
|
17863
18286
|
}
|
|
17864
18287
|
|
|
18288
|
+
/**
|
|
18289
|
+
* Get triangle points as an array of XY coordinates
|
|
18290
|
+
* @private
|
|
18291
|
+
*/
|
|
18292
|
+
_getTrianglePoints() {
|
|
18293
|
+
const widthBy2 = this.width / 2;
|
|
18294
|
+
const heightBy2 = this.height / 2;
|
|
18295
|
+
return [{
|
|
18296
|
+
x: -widthBy2,
|
|
18297
|
+
y: heightBy2
|
|
18298
|
+
},
|
|
18299
|
+
// bottom left
|
|
18300
|
+
{
|
|
18301
|
+
x: 0,
|
|
18302
|
+
y: -heightBy2
|
|
18303
|
+
},
|
|
18304
|
+
// top center
|
|
18305
|
+
{
|
|
18306
|
+
x: widthBy2,
|
|
18307
|
+
y: heightBy2
|
|
18308
|
+
} // bottom right
|
|
18309
|
+
];
|
|
18310
|
+
}
|
|
18311
|
+
|
|
17865
18312
|
/**
|
|
17866
18313
|
* @private
|
|
17867
18314
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
17868
18315
|
*/
|
|
17869
18316
|
_render(ctx) {
|
|
17870
|
-
|
|
17871
|
-
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
18317
|
+
if (this.cornerRadius > 0) {
|
|
18318
|
+
// Render rounded triangle
|
|
18319
|
+
const points = this._getTrianglePoints();
|
|
18320
|
+
const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
|
|
18321
|
+
renderRoundedPolygon(ctx, roundedCorners, true);
|
|
18322
|
+
} else {
|
|
18323
|
+
// Render sharp triangle (original implementation)
|
|
18324
|
+
const widthBy2 = this.width / 2;
|
|
18325
|
+
const heightBy2 = this.height / 2;
|
|
18326
|
+
ctx.beginPath();
|
|
18327
|
+
ctx.moveTo(-widthBy2, heightBy2);
|
|
18328
|
+
ctx.lineTo(0, -heightBy2);
|
|
18329
|
+
ctx.lineTo(widthBy2, heightBy2);
|
|
18330
|
+
ctx.closePath();
|
|
18331
|
+
}
|
|
17877
18332
|
this._renderPaintInOrder(ctx);
|
|
17878
18333
|
}
|
|
17879
18334
|
|
|
18335
|
+
/**
|
|
18336
|
+
* Returns object representation of an instance
|
|
18337
|
+
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
18338
|
+
* @return {Object} object representation of an instance
|
|
18339
|
+
*/
|
|
18340
|
+
toObject() {
|
|
18341
|
+
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
18342
|
+
return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
|
|
18343
|
+
}
|
|
18344
|
+
|
|
17880
18345
|
/**
|
|
17881
18346
|
* Returns svg representation of an instance
|
|
17882
18347
|
* @return {Array} an array of strings with the specific svg representation
|
|
17883
18348
|
* of the instance
|
|
17884
18349
|
*/
|
|
17885
18350
|
_toSVG() {
|
|
17886
|
-
|
|
17887
|
-
|
|
17888
|
-
points =
|
|
17889
|
-
|
|
18351
|
+
if (this.cornerRadius > 0) {
|
|
18352
|
+
// Generate rounded triangle as path
|
|
18353
|
+
const points = this._getTrianglePoints();
|
|
18354
|
+
const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
|
|
18355
|
+
const pathData = generateRoundedPolygonPath(roundedCorners, true);
|
|
18356
|
+
return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
|
|
18357
|
+
} else {
|
|
18358
|
+
// Original sharp triangle implementation
|
|
18359
|
+
const widthBy2 = this.width / 2;
|
|
18360
|
+
const heightBy2 = this.height / 2;
|
|
18361
|
+
const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
|
|
18362
|
+
return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
|
|
18363
|
+
}
|
|
17890
18364
|
}
|
|
17891
18365
|
}
|
|
18366
|
+
/**
|
|
18367
|
+
* Corner radius for rounded triangle corners
|
|
18368
|
+
* @type Number
|
|
18369
|
+
*/
|
|
17892
18370
|
_defineProperty(Triangle, "type", 'Triangle');
|
|
18371
|
+
_defineProperty(Triangle, "cacheProperties", [...cacheProperties, ...TRIANGLE_PROPS]);
|
|
17893
18372
|
_defineProperty(Triangle, "ownDefaults", triangleDefaultValues);
|
|
17894
18373
|
classRegistry.setClass(Triangle);
|
|
17895
18374
|
classRegistry.setSVGClass(Triangle);
|
|
@@ -18054,7 +18533,8 @@
|
|
|
18054
18533
|
/**
|
|
18055
18534
|
* @deprecated transient option soon to be removed in favor of a different design
|
|
18056
18535
|
*/
|
|
18057
|
-
exactBoundingBox: false
|
|
18536
|
+
exactBoundingBox: false,
|
|
18537
|
+
cornerRadius: 0
|
|
18058
18538
|
};
|
|
18059
18539
|
class Polyline extends FabricObject {
|
|
18060
18540
|
static getDefaults() {
|
|
@@ -18268,7 +18748,7 @@
|
|
|
18268
18748
|
toObject() {
|
|
18269
18749
|
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
18270
18750
|
return {
|
|
18271
|
-
...super.toObject(propertiesToInclude),
|
|
18751
|
+
...super.toObject(['cornerRadius', ...propertiesToInclude]),
|
|
18272
18752
|
points: this.points.map(_ref => {
|
|
18273
18753
|
let {
|
|
18274
18754
|
x,
|
|
@@ -18288,14 +18768,28 @@
|
|
|
18288
18768
|
* of the instance
|
|
18289
18769
|
*/
|
|
18290
18770
|
_toSVG() {
|
|
18291
|
-
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
|
|
18771
|
+
if (this.cornerRadius > 0 && this.points.length >= 3) {
|
|
18772
|
+
// Generate rounded polygon/polyline as path
|
|
18773
|
+
const diffX = this.pathOffset.x;
|
|
18774
|
+
const diffY = this.pathOffset.y;
|
|
18775
|
+
const adjustedPoints = this.points.map(point => ({
|
|
18776
|
+
x: point.x - diffX,
|
|
18777
|
+
y: point.y - diffY
|
|
18778
|
+
}));
|
|
18779
|
+
const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
|
|
18780
|
+
const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
|
|
18781
|
+
return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
|
|
18782
|
+
} else {
|
|
18783
|
+
// Original sharp corners implementation
|
|
18784
|
+
const points = [];
|
|
18785
|
+
const diffX = this.pathOffset.x;
|
|
18786
|
+
const diffY = this.pathOffset.y;
|
|
18787
|
+
const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
|
|
18788
|
+
for (let i = 0, len = this.points.length; i < len; i++) {
|
|
18789
|
+
points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
|
|
18790
|
+
}
|
|
18791
|
+
return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
|
|
18297
18792
|
}
|
|
18298
|
-
return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
|
|
18299
18793
|
}
|
|
18300
18794
|
|
|
18301
18795
|
/**
|
|
@@ -18311,13 +18805,24 @@
|
|
|
18311
18805
|
// NaN comes from parseFloat of a empty string in parser
|
|
18312
18806
|
return;
|
|
18313
18807
|
}
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
|
|
18808
|
+
if (this.cornerRadius > 0 && len >= 3) {
|
|
18809
|
+
// Render with rounded corners
|
|
18810
|
+
const adjustedPoints = this.points.map(point => ({
|
|
18811
|
+
x: point.x - x,
|
|
18812
|
+
y: point.y - y
|
|
18813
|
+
}));
|
|
18814
|
+
const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
|
|
18815
|
+
renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
|
|
18816
|
+
} else {
|
|
18817
|
+
// Original sharp corners implementation
|
|
18818
|
+
ctx.beginPath();
|
|
18819
|
+
ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
|
|
18820
|
+
for (let i = 0; i < len; i++) {
|
|
18821
|
+
const point = this.points[i];
|
|
18822
|
+
ctx.lineTo(point.x - x, point.y - y);
|
|
18823
|
+
}
|
|
18824
|
+
!this.isOpen() && ctx.closePath();
|
|
18319
18825
|
}
|
|
18320
|
-
!this.isOpen() && ctx.closePath();
|
|
18321
18826
|
this._renderPaintInOrder(ctx);
|
|
18322
18827
|
}
|
|
18323
18828
|
|
|
@@ -18382,10 +18887,15 @@
|
|
|
18382
18887
|
* @type Boolean
|
|
18383
18888
|
* @default false
|
|
18384
18889
|
*/
|
|
18890
|
+
/**
|
|
18891
|
+
* Corner radius for rounded corners
|
|
18892
|
+
* @type Number
|
|
18893
|
+
* @default 0
|
|
18894
|
+
*/
|
|
18385
18895
|
_defineProperty(Polyline, "ownDefaults", polylineDefaultValues);
|
|
18386
18896
|
_defineProperty(Polyline, "type", 'Polyline');
|
|
18387
18897
|
_defineProperty(Polyline, "layoutProperties", [SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points']);
|
|
18388
|
-
_defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points']);
|
|
18898
|
+
_defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points', 'cornerRadius']);
|
|
18389
18899
|
_defineProperty(Polyline, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES]);
|
|
18390
18900
|
classRegistry.setClass(Polyline);
|
|
18391
18901
|
classRegistry.setSVGClass(Polyline);
|
|
@@ -18769,6 +19279,97 @@
|
|
|
18769
19279
|
};
|
|
18770
19280
|
}
|
|
18771
19281
|
|
|
19282
|
+
/**
|
|
19283
|
+
* Get a representative character for font metrics measurement
|
|
19284
|
+
* Uses canvas to test which scripts the font actually supports
|
|
19285
|
+
*/
|
|
19286
|
+
function getRepresentativeCharacter(fontFamily) {
|
|
19287
|
+
const context = getMeasurementContext();
|
|
19288
|
+
|
|
19289
|
+
// Wait for font to be ready if possible
|
|
19290
|
+
if (typeof document !== 'undefined' && 'fonts' in document) {
|
|
19291
|
+
try {
|
|
19292
|
+
// Check if font is ready, if not, use fallback immediately
|
|
19293
|
+
if (!document.fonts.check(`16px ${fontFamily}`)) {
|
|
19294
|
+
return 'M'; // Use safe fallback while font loads
|
|
19295
|
+
}
|
|
19296
|
+
} catch (e) {
|
|
19297
|
+
// Font check failed, use fallback
|
|
19298
|
+
return 'M';
|
|
19299
|
+
}
|
|
19300
|
+
}
|
|
19301
|
+
|
|
19302
|
+
// Test characters for different scripts
|
|
19303
|
+
const testChars = [{
|
|
19304
|
+
char: 'م',
|
|
19305
|
+
script: 'Arabic'
|
|
19306
|
+
},
|
|
19307
|
+
// Arabic
|
|
19308
|
+
{
|
|
19309
|
+
char: 'א',
|
|
19310
|
+
script: 'Hebrew'
|
|
19311
|
+
},
|
|
19312
|
+
// Hebrew
|
|
19313
|
+
{
|
|
19314
|
+
char: 'अ',
|
|
19315
|
+
script: 'Devanagari'
|
|
19316
|
+
},
|
|
19317
|
+
// Hindi/Sanskrit
|
|
19318
|
+
{
|
|
19319
|
+
char: 'ا',
|
|
19320
|
+
script: 'Urdu'
|
|
19321
|
+
},
|
|
19322
|
+
// Urdu
|
|
19323
|
+
{
|
|
19324
|
+
char: 'ک',
|
|
19325
|
+
script: 'Persian'
|
|
19326
|
+
},
|
|
19327
|
+
// Persian
|
|
19328
|
+
{
|
|
19329
|
+
char: 'த',
|
|
19330
|
+
script: 'Tamil'
|
|
19331
|
+
},
|
|
19332
|
+
// Tamil
|
|
19333
|
+
{
|
|
19334
|
+
char: 'ก',
|
|
19335
|
+
script: 'Thai'
|
|
19336
|
+
},
|
|
19337
|
+
// Thai
|
|
19338
|
+
{
|
|
19339
|
+
char: 'М',
|
|
19340
|
+
script: 'Cyrillic'
|
|
19341
|
+
},
|
|
19342
|
+
// Cyrillic
|
|
19343
|
+
{
|
|
19344
|
+
char: 'Ω',
|
|
19345
|
+
script: 'Greek'
|
|
19346
|
+
},
|
|
19347
|
+
// Greek
|
|
19348
|
+
{
|
|
19349
|
+
char: 'M',
|
|
19350
|
+
script: 'Latin'
|
|
19351
|
+
} // Latin (fallback)
|
|
19352
|
+
];
|
|
19353
|
+
|
|
19354
|
+
// Set the font
|
|
19355
|
+
context.font = `16px ${fontFamily}`;
|
|
19356
|
+
|
|
19357
|
+
// Test each character to see which ones render properly
|
|
19358
|
+
// Use a more robust width check to avoid false positives
|
|
19359
|
+
const fallbackWidth = context.measureText('M').width;
|
|
19360
|
+
for (const test of testChars) {
|
|
19361
|
+
const metrics = context.measureText(test.char);
|
|
19362
|
+
|
|
19363
|
+
// Character is valid if it has width and isn't just a fallback glyph
|
|
19364
|
+
if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
|
|
19365
|
+
return test.char;
|
|
19366
|
+
}
|
|
19367
|
+
}
|
|
19368
|
+
|
|
19369
|
+
// Fallback to Latin 'M'
|
|
19370
|
+
return 'M';
|
|
19371
|
+
}
|
|
19372
|
+
|
|
18772
19373
|
/**
|
|
18773
19374
|
* Get font metrics for layout calculations
|
|
18774
19375
|
*/
|
|
@@ -18782,8 +19383,9 @@
|
|
|
18782
19383
|
const context = getMeasurementContext();
|
|
18783
19384
|
applyFontStyle(context, options);
|
|
18784
19385
|
|
|
18785
|
-
// Use
|
|
18786
|
-
const
|
|
19386
|
+
// Use representative character based on font's primary script
|
|
19387
|
+
const sample = getRepresentativeCharacter(options.fontFamily);
|
|
19388
|
+
const metrics = context.measureText(sample);
|
|
18787
19389
|
const fontSize = options.fontSize;
|
|
18788
19390
|
|
|
18789
19391
|
// Calculate metrics with fallbacks
|
|
@@ -18835,7 +19437,11 @@
|
|
|
18835
19437
|
} = options;
|
|
18836
19438
|
|
|
18837
19439
|
// Normalize font family (add quotes if needed)
|
|
18838
|
-
|
|
19440
|
+
let normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
|
|
19441
|
+
|
|
19442
|
+
// Note: Font fallbacks are handled in the rendering phase only
|
|
19443
|
+
// to avoid affecting measurement calculations for text wrapping
|
|
19444
|
+
|
|
18839
19445
|
return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
|
|
18840
19446
|
}
|
|
18841
19447
|
|
|
@@ -18987,6 +19593,81 @@
|
|
|
18987
19593
|
const kerningCache = new KerningCache();
|
|
18988
19594
|
const fontMetricsCache = new FontMetricsCache();
|
|
18989
19595
|
|
|
19596
|
+
// Set up font loading listener to clear caches when fonts change
|
|
19597
|
+
if (typeof document !== 'undefined' && 'fonts' in document) {
|
|
19598
|
+
document.fonts.addEventListener('loadingdone', () => {
|
|
19599
|
+
// Clear all caches when fonts finish loading
|
|
19600
|
+
clearAllCaches();
|
|
19601
|
+
});
|
|
19602
|
+
}
|
|
19603
|
+
|
|
19604
|
+
/**
|
|
19605
|
+
* Clear all measurement caches
|
|
19606
|
+
*/
|
|
19607
|
+
function clearAllCaches() {
|
|
19608
|
+
measurementCache.clear();
|
|
19609
|
+
kerningCache.clear();
|
|
19610
|
+
fontMetricsCache.clear();
|
|
19611
|
+
}
|
|
19612
|
+
|
|
19613
|
+
/**
|
|
19614
|
+
* Detect if a font lacks English glyph support
|
|
19615
|
+
* These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
|
|
19616
|
+
*/
|
|
19617
|
+
function fontLacksEnglishGlyphs(fontFamily) {
|
|
19618
|
+
if (typeof document === 'undefined') return false;
|
|
19619
|
+
|
|
19620
|
+
// Known fonts that lack English glyphs
|
|
19621
|
+
const knownNonEnglishFonts = ['stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani', 'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'];
|
|
19622
|
+
const lowerFontFamily = fontFamily.toLowerCase();
|
|
19623
|
+
|
|
19624
|
+
// Check known list first
|
|
19625
|
+
if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
|
|
19626
|
+
return true;
|
|
19627
|
+
}
|
|
19628
|
+
|
|
19629
|
+
// Dynamic glyph support detection
|
|
19630
|
+
const context = getMeasurementContext();
|
|
19631
|
+
context.font = `16px ${fontFamily}`;
|
|
19632
|
+
|
|
19633
|
+
// Test English characters
|
|
19634
|
+
const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
|
|
19635
|
+
const fallbackFont = 'Arial, sans-serif';
|
|
19636
|
+
|
|
19637
|
+
// Measure with target font
|
|
19638
|
+
const targetWidths = englishChars.map(char => context.measureText(char).width);
|
|
19639
|
+
|
|
19640
|
+
// Measure with fallback font
|
|
19641
|
+
context.font = `16px ${fallbackFont}`;
|
|
19642
|
+
const fallbackWidths = englishChars.map(char => context.measureText(char).width);
|
|
19643
|
+
|
|
19644
|
+
// If most measurements are identical, the font likely doesn't have English glyphs
|
|
19645
|
+
let identicalCount = 0;
|
|
19646
|
+
for (let i = 0; i < englishChars.length; i++) {
|
|
19647
|
+
if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
|
|
19648
|
+
identicalCount++;
|
|
19649
|
+
}
|
|
19650
|
+
}
|
|
19651
|
+
const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
|
|
19652
|
+
const lacksSupport = identicalCount >= lacksSupportThreshold;
|
|
19653
|
+
return lacksSupport;
|
|
19654
|
+
}
|
|
19655
|
+
|
|
19656
|
+
// Cache for font glyph detection results
|
|
19657
|
+
const fontGlyphCache = new Map();
|
|
19658
|
+
|
|
19659
|
+
/**
|
|
19660
|
+
* Cached version of font glyph detection
|
|
19661
|
+
*/
|
|
19662
|
+
function fontLacksEnglishGlyphsCached(fontFamily) {
|
|
19663
|
+
if (fontGlyphCache.has(fontFamily)) {
|
|
19664
|
+
return fontGlyphCache.get(fontFamily);
|
|
19665
|
+
}
|
|
19666
|
+
const result = fontLacksEnglishGlyphs(fontFamily);
|
|
19667
|
+
fontGlyphCache.set(fontFamily, result);
|
|
19668
|
+
return result;
|
|
19669
|
+
}
|
|
19670
|
+
|
|
18990
19671
|
/**
|
|
18991
19672
|
* Unicode and Internationalization Support
|
|
18992
19673
|
*
|
|
@@ -20170,6 +20851,15 @@
|
|
|
20170
20851
|
* Does not return dimensions.
|
|
20171
20852
|
*/
|
|
20172
20853
|
initDimensions() {
|
|
20854
|
+
// Check if font is ready for accurate measurements
|
|
20855
|
+
// Only block initialization if it's a critical font loading situation
|
|
20856
|
+
const fontReady = this._isFontReady();
|
|
20857
|
+
if (!fontReady && !this.initialized) {
|
|
20858
|
+
// Only schedule font loading on first initialization
|
|
20859
|
+
this._scheduleInitAfterFontLoad();
|
|
20860
|
+
// Continue with fallback measurements for now
|
|
20861
|
+
}
|
|
20862
|
+
|
|
20173
20863
|
// Use advanced layout if enabled
|
|
20174
20864
|
if (this.enableAdvancedLayout && !this.path) {
|
|
20175
20865
|
return this.initDimensionsAdvanced();
|
|
@@ -20186,7 +20876,21 @@
|
|
|
20186
20876
|
}
|
|
20187
20877
|
if (this.textAlign.includes(JUSTIFY)) {
|
|
20188
20878
|
// once text is measured we need to make space fatter to make justified text.
|
|
20189
|
-
|
|
20879
|
+
// Ensure __charBounds exists before calling enlargeSpaces
|
|
20880
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
20881
|
+
this.enlargeSpaces();
|
|
20882
|
+
} else {
|
|
20883
|
+
console.warn('⚠️ __charBounds not ready for justify alignment, deferring enlargeSpaces');
|
|
20884
|
+
// Defer the justify calculation until the next frame
|
|
20885
|
+
setTimeout(() => {
|
|
20886
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
20887
|
+
var _this$canvas;
|
|
20888
|
+
console.log('🔧 Applying deferred justify alignment');
|
|
20889
|
+
this.enlargeSpaces();
|
|
20890
|
+
(_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
|
|
20891
|
+
}
|
|
20892
|
+
}, 0);
|
|
20893
|
+
}
|
|
20190
20894
|
}
|
|
20191
20895
|
}
|
|
20192
20896
|
|
|
@@ -20195,8 +20899,9 @@
|
|
|
20195
20899
|
*/
|
|
20196
20900
|
enlargeSpaces() {
|
|
20197
20901
|
let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
|
|
20902
|
+
const isRtl = this.direction === 'rtl';
|
|
20198
20903
|
for (let i = 0, len = this._textLines.length; i < len; i++) {
|
|
20199
|
-
if (this.textAlign
|
|
20904
|
+
if (!this.textAlign.includes('justify') && (i === len - 1 || this.isEndOfWrapping(i))) {
|
|
20200
20905
|
continue;
|
|
20201
20906
|
}
|
|
20202
20907
|
accumulatedSpace = 0;
|
|
@@ -20205,15 +20910,47 @@
|
|
|
20205
20910
|
if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
|
|
20206
20911
|
numberOfSpaces = spaces.length;
|
|
20207
20912
|
diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
|
|
20208
|
-
|
|
20209
|
-
|
|
20210
|
-
|
|
20211
|
-
|
|
20212
|
-
|
|
20213
|
-
|
|
20214
|
-
|
|
20215
|
-
|
|
20216
|
-
|
|
20913
|
+
console.log(`🔧 EnlargeSpaces Line ${i}:`);
|
|
20914
|
+
console.log(` Current width: ${currentLineWidth}, Target: ${this.width}`);
|
|
20915
|
+
console.log(` Spaces: ${numberOfSpaces}, diffSpace: ${diffSpace.toFixed(2)}`);
|
|
20916
|
+
if (isRtl) {
|
|
20917
|
+
for (let j = 0; j < line.length; j++) {
|
|
20918
|
+
if (this._reSpaceAndTab.test(line[j])) ;
|
|
20919
|
+
}
|
|
20920
|
+
|
|
20921
|
+
// For RTL, we need to work backwards through the visual positions
|
|
20922
|
+
// but still update logical positions correctly
|
|
20923
|
+
let spaceCount = 0;
|
|
20924
|
+
for (let j = 0; j <= line.length; j++) {
|
|
20925
|
+
charBound = this.__charBounds[i][j];
|
|
20926
|
+
if (charBound) {
|
|
20927
|
+
if (this._reSpaceAndTab.test(line[j])) {
|
|
20928
|
+
charBound.width += diffSpace;
|
|
20929
|
+
charBound.kernedWidth += diffSpace;
|
|
20930
|
+
spaceCount++;
|
|
20931
|
+
}
|
|
20932
|
+
|
|
20933
|
+
// For RTL, shift all characters to the right by the total expansion
|
|
20934
|
+
// minus the expansion that comes after this character
|
|
20935
|
+
const remainingSpaces = numberOfSpaces - spaceCount;
|
|
20936
|
+
const shiftAmount = remainingSpaces * diffSpace;
|
|
20937
|
+
charBound.left += shiftAmount;
|
|
20938
|
+
}
|
|
20939
|
+
}
|
|
20940
|
+
} else {
|
|
20941
|
+
// LTR processing (original logic)
|
|
20942
|
+
for (let j = 0; j <= line.length; j++) {
|
|
20943
|
+
charBound = this.__charBounds[i][j];
|
|
20944
|
+
if (charBound) {
|
|
20945
|
+
if (this._reSpaceAndTab.test(line[j])) {
|
|
20946
|
+
charBound.width += diffSpace;
|
|
20947
|
+
charBound.kernedWidth += diffSpace;
|
|
20948
|
+
charBound.left += accumulatedSpace;
|
|
20949
|
+
accumulatedSpace += diffSpace;
|
|
20950
|
+
} else {
|
|
20951
|
+
charBound.left += accumulatedSpace;
|
|
20952
|
+
}
|
|
20953
|
+
}
|
|
20217
20954
|
}
|
|
20218
20955
|
}
|
|
20219
20956
|
}
|
|
@@ -20291,6 +21028,18 @@
|
|
|
20291
21028
|
|
|
20292
21029
|
// Convert layout to legacy format for compatibility
|
|
20293
21030
|
this._convertLayoutToLegacyFormat(layout);
|
|
21031
|
+
|
|
21032
|
+
// Ensure justify alignment is properly applied for compatibility with legacy rendering
|
|
21033
|
+
if (this.textAlign.includes(JUSTIFY)) {
|
|
21034
|
+
// Force enlarge spaces after advanced layout calculation
|
|
21035
|
+
setTimeout(() => {
|
|
21036
|
+
if (this.enlargeSpaces) {
|
|
21037
|
+
var _this$canvas2;
|
|
21038
|
+
this.enlargeSpaces();
|
|
21039
|
+
(_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.renderAll();
|
|
21040
|
+
}
|
|
21041
|
+
}, 0);
|
|
21042
|
+
}
|
|
20294
21043
|
this.dirty = true;
|
|
20295
21044
|
}
|
|
20296
21045
|
|
|
@@ -20871,7 +21620,15 @@
|
|
|
20871
21620
|
if (currentDirection !== this.direction) {
|
|
20872
21621
|
ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
|
|
20873
21622
|
ctx.direction = isLtr ? 'ltr' : 'rtl';
|
|
20874
|
-
|
|
21623
|
+
|
|
21624
|
+
// For justify alignments, we need to set the correct canvas text alignment
|
|
21625
|
+
// This is crucial for RTL text to render in the correct order
|
|
21626
|
+
if (isJustify) {
|
|
21627
|
+
// Justify uses LEFT alignment as a base, letting the character positioning handle justification
|
|
21628
|
+
ctx.textAlign = LEFT;
|
|
21629
|
+
} else {
|
|
21630
|
+
ctx.textAlign = isLtr ? LEFT : RIGHT;
|
|
21631
|
+
}
|
|
20875
21632
|
}
|
|
20876
21633
|
top -= lineHeight * this._fontSizeFraction / this.lineHeight;
|
|
20877
21634
|
if (shortCut) {
|
|
@@ -21107,9 +21864,21 @@
|
|
|
21107
21864
|
direction = this.direction,
|
|
21108
21865
|
isEndOfWrapping = this.isEndOfWrapping(lineIndex);
|
|
21109
21866
|
let leftOffset = 0;
|
|
21110
|
-
|
|
21111
|
-
|
|
21867
|
+
|
|
21868
|
+
// Handle justify alignments (excluding last lines and wrapped line ends)
|
|
21869
|
+
const isJustifyLine = textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping;
|
|
21870
|
+
if (isJustifyLine) {
|
|
21871
|
+
// Justify lines should start at the left edge for LTR and right edge for RTL
|
|
21872
|
+
// The space distribution is handled by enlargeSpaces()
|
|
21873
|
+
if (direction === 'rtl') {
|
|
21874
|
+
// For RTL justify, we need to account for the line being right-aligned
|
|
21875
|
+
return 0;
|
|
21876
|
+
} else {
|
|
21877
|
+
return 0;
|
|
21878
|
+
}
|
|
21112
21879
|
}
|
|
21880
|
+
|
|
21881
|
+
// Handle non-justify alignments
|
|
21113
21882
|
if (textAlign === CENTER) {
|
|
21114
21883
|
leftOffset = lineDiff / 2;
|
|
21115
21884
|
}
|
|
@@ -21122,6 +21891,8 @@
|
|
|
21122
21891
|
if (textAlign === JUSTIFY_RIGHT) {
|
|
21123
21892
|
leftOffset = lineDiff;
|
|
21124
21893
|
}
|
|
21894
|
+
|
|
21895
|
+
// Apply RTL adjustments for non-justify alignments
|
|
21125
21896
|
if (direction === 'rtl') {
|
|
21126
21897
|
if (textAlign === RIGHT || textAlign === JUSTIFY || textAlign === JUSTIFY_RIGHT) {
|
|
21127
21898
|
leftOffset = 0;
|
|
@@ -21280,7 +22051,19 @@
|
|
|
21280
22051
|
fontSize = this.fontSize
|
|
21281
22052
|
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
21282
22053
|
let forMeasuring = arguments.length > 1 ? arguments[1] : undefined;
|
|
21283
|
-
|
|
22054
|
+
let parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
|
|
22055
|
+
|
|
22056
|
+
// For fonts like STV that don't support English/Latin characters,
|
|
22057
|
+
// add fallback fonts for consistent rendering of unsupported characters
|
|
22058
|
+
// Only add fallbacks during actual rendering, not for measurements
|
|
22059
|
+
if (!forMeasuring &&
|
|
22060
|
+
// Only during rendering, not measuring
|
|
22061
|
+
!fontFamily.includes(',') && (
|
|
22062
|
+
// Don't add fallbacks if already has them
|
|
22063
|
+
fontFamily.toLowerCase().includes('stv') || fontFamily.toLowerCase().includes('arabic') || fontFamily.toLowerCase().includes('naskh') || fontFamily.toLowerCase().includes('kufi'))) {
|
|
22064
|
+
// Add fallback fonts for unsupported characters (spaces, punctuation, etc.)
|
|
22065
|
+
parsedFontFamily = `${parsedFontFamily}, "Arial Unicode MS", Arial, sans-serif`;
|
|
22066
|
+
}
|
|
21284
22067
|
return [fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily].join(' ');
|
|
21285
22068
|
}
|
|
21286
22069
|
|
|
@@ -21324,7 +22107,13 @@
|
|
|
21324
22107
|
newLine = ['\n'];
|
|
21325
22108
|
let newText = [];
|
|
21326
22109
|
for (let i = 0; i < lines.length; i++) {
|
|
21327
|
-
|
|
22110
|
+
// Use BiDi-aware grapheme splitting for RTL text
|
|
22111
|
+
if (this.direction === 'rtl' || this._containsArabicText(lines[i])) {
|
|
22112
|
+
newLines[i] = segmentGraphemes(lines[i]);
|
|
22113
|
+
console.log(`🔤 BiDi-aware split line ${i}: "${lines[i]}" -> [${newLines[i].join(', ')}]`);
|
|
22114
|
+
} else {
|
|
22115
|
+
newLines[i] = this.graphemeSplit(lines[i]);
|
|
22116
|
+
}
|
|
21328
22117
|
newText = newText.concat(newLines[i], newLine);
|
|
21329
22118
|
}
|
|
21330
22119
|
newText.pop();
|
|
@@ -21336,6 +22125,14 @@
|
|
|
21336
22125
|
};
|
|
21337
22126
|
}
|
|
21338
22127
|
|
|
22128
|
+
/**
|
|
22129
|
+
* Check if text contains Arabic characters
|
|
22130
|
+
* @private
|
|
22131
|
+
*/
|
|
22132
|
+
_containsArabicText(text) {
|
|
22133
|
+
return /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
22134
|
+
}
|
|
22135
|
+
|
|
21339
22136
|
/**
|
|
21340
22137
|
* Returns object representation of an instance
|
|
21341
22138
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
@@ -21458,6 +22255,88 @@
|
|
|
21458
22255
|
|
|
21459
22256
|
/* _FROM_SVG_END_ */
|
|
21460
22257
|
|
|
22258
|
+
/**
|
|
22259
|
+
* Check if the font is ready for accurate measurements
|
|
22260
|
+
* @private
|
|
22261
|
+
*/
|
|
22262
|
+
_isFontReady() {
|
|
22263
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
22264
|
+
return true; // Assume ready in non-browser environments
|
|
22265
|
+
}
|
|
22266
|
+
try {
|
|
22267
|
+
return document.fonts.check(`${this.fontSize}px ${this.fontFamily}`);
|
|
22268
|
+
} catch (e) {
|
|
22269
|
+
return true; // Fallback to assuming ready if check fails
|
|
22270
|
+
}
|
|
22271
|
+
}
|
|
22272
|
+
|
|
22273
|
+
/**
|
|
22274
|
+
* Schedule re-initialization after font loads
|
|
22275
|
+
* @private
|
|
22276
|
+
*/
|
|
22277
|
+
_scheduleInitAfterFontLoad() {
|
|
22278
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
22279
|
+
return;
|
|
22280
|
+
}
|
|
22281
|
+
|
|
22282
|
+
// Only schedule if not already waiting
|
|
22283
|
+
if (this._fontLoadScheduled) {
|
|
22284
|
+
return;
|
|
22285
|
+
}
|
|
22286
|
+
this._fontLoadScheduled = true;
|
|
22287
|
+
const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
|
|
22288
|
+
document.fonts.load(fontSpec).then(() => {
|
|
22289
|
+
this._fontLoadScheduled = false;
|
|
22290
|
+
// Re-initialize dimensions with proper font metrics
|
|
22291
|
+
this.initDimensions();
|
|
22292
|
+
|
|
22293
|
+
// Extra step for justify alignment after font loading
|
|
22294
|
+
if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
|
|
22295
|
+
setTimeout(() => {
|
|
22296
|
+
var _this$canvas3;
|
|
22297
|
+
if (this.enlargeSpaces) {
|
|
22298
|
+
this.enlargeSpaces();
|
|
22299
|
+
}
|
|
22300
|
+
(_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
|
|
22301
|
+
}, 10);
|
|
22302
|
+
} else {
|
|
22303
|
+
var _this$canvas4;
|
|
22304
|
+
(_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
|
|
22305
|
+
}
|
|
22306
|
+
}).catch(() => {
|
|
22307
|
+
this._fontLoadScheduled = false;
|
|
22308
|
+
});
|
|
22309
|
+
}
|
|
22310
|
+
|
|
22311
|
+
/**
|
|
22312
|
+
* Force complete text re-initialization (useful after JSON loading)
|
|
22313
|
+
*/
|
|
22314
|
+
forceTextReinitialization() {
|
|
22315
|
+
console.log('🔄 Force reinitializing text object');
|
|
22316
|
+
|
|
22317
|
+
// Clear all caches
|
|
22318
|
+
this._clearCache();
|
|
22319
|
+
this.dirty = true;
|
|
22320
|
+
|
|
22321
|
+
// Force text splitting to rebuild internal structures
|
|
22322
|
+
this._splitText();
|
|
22323
|
+
|
|
22324
|
+
// Re-initialize dimensions
|
|
22325
|
+
this.initDimensions();
|
|
22326
|
+
|
|
22327
|
+
// Special handling for justify alignment
|
|
22328
|
+
if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
|
|
22329
|
+
// Ensure justify is applied after dimensions are set
|
|
22330
|
+
setTimeout(() => {
|
|
22331
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
22332
|
+
var _this$canvas5;
|
|
22333
|
+
this.enlargeSpaces();
|
|
22334
|
+
(_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
|
|
22335
|
+
}
|
|
22336
|
+
}, 10);
|
|
22337
|
+
}
|
|
22338
|
+
}
|
|
22339
|
+
|
|
21461
22340
|
/**
|
|
21462
22341
|
* Returns FabricText instance from an object representation
|
|
21463
22342
|
* @param {Object} object plain js Object to create an instance from
|
|
@@ -21469,6 +22348,93 @@
|
|
|
21469
22348
|
styles: stylesFromArray(object.styles || {}, object.text)
|
|
21470
22349
|
}, {
|
|
21471
22350
|
extraParam: 'text'
|
|
22351
|
+
}).then(textObject => {
|
|
22352
|
+
// Ensure text object is properly initialized after JSON deserialization
|
|
22353
|
+
// This is critical for justify alignment and other text layout features
|
|
22354
|
+
textObject.initialized = true;
|
|
22355
|
+
|
|
22356
|
+
// Force reinitialization to ensure proper layout
|
|
22357
|
+
if (textObject._clearCache) {
|
|
22358
|
+
textObject._clearCache();
|
|
22359
|
+
}
|
|
22360
|
+
textObject.dirty = true;
|
|
22361
|
+
|
|
22362
|
+
// Check if we need to wait for font loading (especially for custom fonts like STV)
|
|
22363
|
+
const fontSpec = `${textObject.fontSize}px ${textObject.fontFamily}`;
|
|
22364
|
+
|
|
22365
|
+
// For custom fonts, ensure they're loaded before initializing dimensions
|
|
22366
|
+
if (typeof document !== 'undefined' && 'fonts' in document && textObject.fontFamily !== 'Arial' && textObject.fontFamily !== 'Times New Roman') {
|
|
22367
|
+
return document.fonts.load(fontSpec).then(() => {
|
|
22368
|
+
var _textObject$fontFamil;
|
|
22369
|
+
console.log(`🔤 Font loaded for JSON object: ${fontSpec}`);
|
|
22370
|
+
// Ensure initialized flag is set again (in case constructor reset it)
|
|
22371
|
+
textObject.initialized = true;
|
|
22372
|
+
|
|
22373
|
+
// Special handling for STV fonts which have measurement issues
|
|
22374
|
+
const isStvFont = (_textObject$fontFamil = textObject.fontFamily) === null || _textObject$fontFamil === void 0 ? void 0 : _textObject$fontFamil.toLowerCase().includes('stv');
|
|
22375
|
+
if (isStvFont) {
|
|
22376
|
+
console.log(`🔤 STV font detected, using enhanced reinitialization`);
|
|
22377
|
+
|
|
22378
|
+
// Clear all cached state that might interfere with browser wrapping
|
|
22379
|
+
textObject._browserWrapCache = null;
|
|
22380
|
+
textObject._lastDimensionState = null;
|
|
22381
|
+
textObject._browserWrapInitialized = false;
|
|
22382
|
+
console.log(`🔤 STV font: Cleared all cached states for fresh initialization`);
|
|
22383
|
+
|
|
22384
|
+
// Force browser wrapping flag for STV fonts
|
|
22385
|
+
textObject._usingBrowserWrapping = true;
|
|
22386
|
+
console.log(`🔤 STV font: Forcing browser wrapping flag during JSON load`);
|
|
22387
|
+
|
|
22388
|
+
// Multiple initialization attempts for STV fonts
|
|
22389
|
+
const reinitWithDelay = attempt => {
|
|
22390
|
+
if (textObject.forceTextReinitialization) {
|
|
22391
|
+
textObject.forceTextReinitialization();
|
|
22392
|
+
} else {
|
|
22393
|
+
textObject.initDimensions();
|
|
22394
|
+
}
|
|
22395
|
+
|
|
22396
|
+
// Check if width is still problematic after initialization
|
|
22397
|
+
if (textObject.width < 50 && attempt < 3) {
|
|
22398
|
+
console.log(`🔤 STV font width still ${textObject.width}px, retrying in ${100 * attempt}ms (attempt ${attempt + 1}/3)`);
|
|
22399
|
+
setTimeout(() => reinitWithDelay(attempt + 1), 100 * attempt);
|
|
22400
|
+
}
|
|
22401
|
+
};
|
|
22402
|
+
reinitWithDelay(0);
|
|
22403
|
+
} else {
|
|
22404
|
+
// Use specialized reinitialization for Textbox objects
|
|
22405
|
+
if (textObject.forceTextReinitialization) {
|
|
22406
|
+
console.log(`🔤 Using Textbox specialized reinitialization`);
|
|
22407
|
+
textObject.forceTextReinitialization();
|
|
22408
|
+
} else {
|
|
22409
|
+
// Reinitialize dimensions with proper font metrics
|
|
22410
|
+
textObject.initDimensions();
|
|
22411
|
+
}
|
|
22412
|
+
}
|
|
22413
|
+
return textObject;
|
|
22414
|
+
}).catch(() => {
|
|
22415
|
+
console.warn(`⚠️ Font loading failed for ${fontSpec}, proceeding with fallback`);
|
|
22416
|
+
// Ensure initialized flag is set again
|
|
22417
|
+
textObject.initialized = true;
|
|
22418
|
+
|
|
22419
|
+
// Still initialize dimensions even if font loading fails
|
|
22420
|
+
if (textObject.forceTextReinitialization) {
|
|
22421
|
+
textObject.forceTextReinitialization();
|
|
22422
|
+
} else {
|
|
22423
|
+
textObject.initDimensions();
|
|
22424
|
+
}
|
|
22425
|
+
return textObject;
|
|
22426
|
+
});
|
|
22427
|
+
} else {
|
|
22428
|
+
// Standard fonts - ensure initialized and use appropriate method
|
|
22429
|
+
textObject.initialized = true;
|
|
22430
|
+
if (textObject.forceTextReinitialization) {
|
|
22431
|
+
console.log(`🔤 Using Textbox specialized reinitialization for standard font`);
|
|
22432
|
+
textObject.forceTextReinitialization();
|
|
22433
|
+
} else {
|
|
22434
|
+
textObject.initDimensions();
|
|
22435
|
+
}
|
|
22436
|
+
return textObject;
|
|
22437
|
+
}
|
|
21472
22438
|
});
|
|
21473
22439
|
}
|
|
21474
22440
|
}
|
|
@@ -21908,8 +22874,12 @@
|
|
|
21908
22874
|
this.textarea.style.pointerEvents = 'auto';
|
|
21909
22875
|
// Set appropriate unicodeBidi based on content and direction
|
|
21910
22876
|
const hasArabicText = /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(this.target.text || '');
|
|
22877
|
+
const hasLatinText = /[a-zA-Z]/.test(this.target.text || '');
|
|
21911
22878
|
const isLTRDirection = this.target.direction === 'ltr';
|
|
21912
|
-
if (hasArabicText && isLTRDirection) {
|
|
22879
|
+
if (hasArabicText && hasLatinText && isLTRDirection) {
|
|
22880
|
+
// For mixed Arabic/Latin text in LTR mode, use embed for consistent line wrapping
|
|
22881
|
+
this.textarea.style.unicodeBidi = 'embed';
|
|
22882
|
+
} else if (hasArabicText && isLTRDirection) {
|
|
21913
22883
|
// For Arabic text in LTR mode, use embed to preserve shaping while respecting direction
|
|
21914
22884
|
this.textarea.style.unicodeBidi = 'embed';
|
|
21915
22885
|
} else {
|
|
@@ -21998,14 +22968,26 @@
|
|
|
21998
22968
|
parseFloat(this.hostDiv.style.width) / zoom;
|
|
21999
22969
|
const currentHeight = parseFloat(this.hostDiv.style.height) / zoom;
|
|
22000
22970
|
|
|
22001
|
-
//
|
|
22971
|
+
// Always update height for responsive controls (especially important for line deletion)
|
|
22002
22972
|
const heightDiff = Math.abs(currentHeight - target.height);
|
|
22003
|
-
const threshold =
|
|
22973
|
+
const threshold = 0.5; // Lower threshold for better responsiveness to line changes
|
|
22004
22974
|
|
|
22005
22975
|
if (heightDiff > threshold) {
|
|
22976
|
+
target.height;
|
|
22006
22977
|
target.height = currentHeight;
|
|
22007
22978
|
target.setCoords(); // Update control positions
|
|
22979
|
+
|
|
22980
|
+
// Force dirty to ensure proper re-rendering
|
|
22981
|
+
target.dirty = true;
|
|
22008
22982
|
this.canvas.requestRenderAll(); // Re-render to show updated selection
|
|
22983
|
+
|
|
22984
|
+
// IMPORTANT: Reposition overlay after height change
|
|
22985
|
+
requestAnimationFrame(() => {
|
|
22986
|
+
if (!this.isDestroyed) {
|
|
22987
|
+
this.applyOverlayStyle();
|
|
22988
|
+
console.log('📐 Height changed - rechecking alignment after repositioning:');
|
|
22989
|
+
}
|
|
22990
|
+
});
|
|
22009
22991
|
}
|
|
22010
22992
|
}
|
|
22011
22993
|
|
|
@@ -22033,14 +23015,6 @@
|
|
|
22033
23015
|
target.setCoords();
|
|
22034
23016
|
const aCoords = target.aCoords;
|
|
22035
23017
|
|
|
22036
|
-
// DEBUG: Log dimensions before edit
|
|
22037
|
-
console.log('BEFORE EDIT:');
|
|
22038
|
-
console.log(' target.width =', target.width);
|
|
22039
|
-
console.log(' target.height =', target.height);
|
|
22040
|
-
console.log(' target.getScaledWidth() =', target.getScaledWidth());
|
|
22041
|
-
console.log(' target.getScaledHeight() =', target.getScaledHeight());
|
|
22042
|
-
console.log(' target.padding =', target.padding);
|
|
22043
|
-
|
|
22044
23018
|
// 2. Get canvas position and scroll offsets (like rtl-test.html)
|
|
22045
23019
|
const canvasEl = canvas.upperCanvasEl;
|
|
22046
23020
|
const canvasRect = canvasEl.getBoundingClientRect();
|
|
@@ -22063,14 +23037,12 @@
|
|
|
22063
23037
|
const left = canvasRect.left + scrollX + screenPoint.x;
|
|
22064
23038
|
const top = canvasRect.top + scrollY + screenPoint.y;
|
|
22065
23039
|
|
|
22066
|
-
// 4.
|
|
22067
|
-
|
|
22068
|
-
|
|
22069
|
-
|
|
22070
|
-
|
|
22071
|
-
|
|
22072
|
-
console.log(' zoom =', zoom);
|
|
22073
|
-
console.log(' final width =', width);
|
|
23040
|
+
// 4. Calculate the precise width and height for the container
|
|
23041
|
+
// **THE FIX:** Use getBoundingRect() for BOTH width and height.
|
|
23042
|
+
// This is the most reliable measure of the object's final rendered dimensions.
|
|
23043
|
+
const objectBounds = target.getBoundingRect();
|
|
23044
|
+
const width = Math.round(objectBounds.width * zoom);
|
|
23045
|
+
const height = Math.round(objectBounds.height * zoom);
|
|
22074
23046
|
|
|
22075
23047
|
// 5. Apply styles to host DIV - absolute positioning like rtl-test.html
|
|
22076
23048
|
this.hostDiv.style.position = 'absolute';
|
|
@@ -22094,50 +23066,333 @@
|
|
|
22094
23066
|
const scaleX = target.scaleX || 1;
|
|
22095
23067
|
const finalFontSize = baseFontSize * scaleX * zoom;
|
|
22096
23068
|
const fabricLineHeight = target.lineHeight || 1.16;
|
|
22097
|
-
//
|
|
22098
|
-
|
|
22099
|
-
|
|
22100
|
-
|
|
22101
|
-
|
|
23069
|
+
// **THE FIX:** Use 'border-box' so the width property includes padding.
|
|
23070
|
+
// This makes alignment much easier and more reliable.
|
|
23071
|
+
this.textarea.style.boxSizing = 'border-box';
|
|
23072
|
+
|
|
23073
|
+
// **THE FIX:** Set the textarea width to be IDENTICAL to the host div's width.
|
|
23074
|
+
// The padding will now be correctly contained *inside* this width.
|
|
23075
|
+
this.textarea.style.width = `${width}px`;
|
|
23076
|
+
this.textarea.style.height = '100%'; // Let hostDiv control height
|
|
22102
23077
|
this.textarea.style.padding = `${paddingY}px ${paddingX}px`;
|
|
23078
|
+
|
|
23079
|
+
// Apply all other font and text styles to match Fabric
|
|
23080
|
+
const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
|
|
23081
|
+
|
|
23082
|
+
// Special handling for text objects loaded from JSON - ensure they're properly initialized
|
|
23083
|
+
if (target.dirty !== false && target.initDimensions) {
|
|
23084
|
+
console.log('🔧 Ensuring text object is properly initialized before overlay editing');
|
|
23085
|
+
// Force re-initialization if the text object seems to be in a dirty state
|
|
23086
|
+
target.initDimensions();
|
|
23087
|
+
}
|
|
22103
23088
|
this.textarea.style.fontSize = `${finalFontSize}px`;
|
|
22104
|
-
this.textarea.style.lineHeight = String(fabricLineHeight);
|
|
23089
|
+
this.textarea.style.lineHeight = String(fabricLineHeight);
|
|
22105
23090
|
this.textarea.style.fontFamily = target.fontFamily || 'Arial';
|
|
22106
23091
|
this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
|
|
22107
23092
|
this.textarea.style.fontStyle = target.fontStyle || 'normal';
|
|
22108
|
-
|
|
23093
|
+
// Handle text alignment and justification
|
|
23094
|
+
const textAlign = target.textAlign || 'left';
|
|
23095
|
+
let cssTextAlign = textAlign;
|
|
23096
|
+
|
|
23097
|
+
// Detect text direction from content for proper justify handling
|
|
23098
|
+
const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
|
|
23099
|
+
|
|
23100
|
+
// DEBUG: Log alignment details
|
|
23101
|
+
console.log('🔍 ALIGNMENT DEBUG:');
|
|
23102
|
+
console.log(' Fabric textAlign:', textAlign);
|
|
23103
|
+
console.log(' Fabric direction:', target.direction);
|
|
23104
|
+
console.log(' Text content:', JSON.stringify(target.text));
|
|
23105
|
+
console.log(' Detected direction:', autoDetectedDirection);
|
|
23106
|
+
|
|
23107
|
+
// Map fabric.js justify to CSS
|
|
23108
|
+
if (textAlign.includes('justify')) {
|
|
23109
|
+
// Try to match fabric.js justify behavior more precisely
|
|
23110
|
+
try {
|
|
23111
|
+
// For justify, we need to replicate fabric.js space expansion
|
|
23112
|
+
// Use CSS justify but with specific settings to match fabric.js better
|
|
23113
|
+
cssTextAlign = 'justify';
|
|
23114
|
+
|
|
23115
|
+
// Set text-align-last based on justify type and detected direction
|
|
23116
|
+
// Smart justify: respect detected direction even when fabric alignment doesn't match
|
|
23117
|
+
if (textAlign === 'justify') {
|
|
23118
|
+
this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
|
|
23119
|
+
} else if (textAlign === 'justify-left') {
|
|
23120
|
+
// If text is RTL but fabric says justify-left, override to justify-right for better UX
|
|
23121
|
+
if (autoDetectedDirection === 'rtl') {
|
|
23122
|
+
this.textarea.style.textAlignLast = 'right';
|
|
23123
|
+
console.log(' → Overrode justify-left to justify-right for RTL text');
|
|
23124
|
+
} else {
|
|
23125
|
+
this.textarea.style.textAlignLast = 'left';
|
|
23126
|
+
}
|
|
23127
|
+
} else if (textAlign === 'justify-right') {
|
|
23128
|
+
// If text is LTR but fabric says justify-right, override to justify-left for better UX
|
|
23129
|
+
if (autoDetectedDirection === 'ltr') {
|
|
23130
|
+
this.textarea.style.textAlignLast = 'left';
|
|
23131
|
+
console.log(' → Overrode justify-right to justify-left for LTR text');
|
|
23132
|
+
} else {
|
|
23133
|
+
this.textarea.style.textAlignLast = 'right';
|
|
23134
|
+
}
|
|
23135
|
+
} else if (textAlign === 'justify-center') {
|
|
23136
|
+
this.textarea.style.textAlignLast = 'center';
|
|
23137
|
+
}
|
|
23138
|
+
|
|
23139
|
+
// Enhanced justify settings for better fabric.js matching
|
|
23140
|
+
this.textarea.style.textJustify = 'inter-word';
|
|
23141
|
+
this.textarea.style.wordSpacing = 'normal';
|
|
23142
|
+
|
|
23143
|
+
// Additional CSS properties for better justify matching
|
|
23144
|
+
this.textarea.style.textAlign = 'justify';
|
|
23145
|
+
this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
|
|
23146
|
+
|
|
23147
|
+
// Try to force better justify behavior
|
|
23148
|
+
this.textarea.style.textJustifyTrim = 'none';
|
|
23149
|
+
this.textarea.style.textAutospace = 'none';
|
|
23150
|
+
console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
|
|
23151
|
+
} catch (error) {
|
|
23152
|
+
console.warn(' → Justify setup failed, falling back to standard alignment:', error);
|
|
23153
|
+
cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
|
|
23154
|
+
}
|
|
23155
|
+
} else {
|
|
23156
|
+
this.textarea.style.textAlignLast = 'auto';
|
|
23157
|
+
this.textarea.style.textJustify = 'auto';
|
|
23158
|
+
this.textarea.style.wordSpacing = 'normal';
|
|
23159
|
+
console.log(' → Applied standard alignment:', cssTextAlign);
|
|
23160
|
+
}
|
|
23161
|
+
this.textarea.style.textAlign = cssTextAlign;
|
|
22109
23162
|
this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
|
|
22110
|
-
this.textarea.style.letterSpacing = `${
|
|
22111
|
-
this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
|
|
23163
|
+
this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
|
|
22112
23164
|
|
|
22113
|
-
//
|
|
23165
|
+
// Use the already detected direction from above
|
|
23166
|
+
const fabricDirection = target.direction;
|
|
23167
|
+
|
|
23168
|
+
// Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
|
|
23169
|
+
this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
|
|
22114
23170
|
this.textarea.style.fontVariant = 'normal';
|
|
22115
23171
|
this.textarea.style.fontStretch = 'normal';
|
|
22116
|
-
this.textarea.style.textRendering = 'auto';
|
|
22117
|
-
this.textarea.style.fontKerning = '
|
|
22118
|
-
this.textarea.style.
|
|
23172
|
+
this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
|
|
23173
|
+
this.textarea.style.fontKerning = 'normal';
|
|
23174
|
+
this.textarea.style.fontFeatureSettings = 'normal';
|
|
23175
|
+
this.textarea.style.fontVariationSettings = 'normal';
|
|
22119
23176
|
this.textarea.style.margin = '0';
|
|
22120
23177
|
this.textarea.style.border = 'none';
|
|
22121
23178
|
this.textarea.style.outline = 'none';
|
|
22122
23179
|
this.textarea.style.background = 'transparent';
|
|
22123
|
-
this.textarea.style.
|
|
23180
|
+
this.textarea.style.overflowWrap = 'break-word';
|
|
22124
23181
|
this.textarea.style.whiteSpace = 'pre-wrap';
|
|
22125
|
-
|
|
22126
|
-
|
|
22127
|
-
|
|
22128
|
-
console.log('
|
|
22129
|
-
console.log('
|
|
22130
|
-
console.log('
|
|
22131
|
-
console.log('
|
|
22132
|
-
console.log('
|
|
22133
|
-
console.log('
|
|
22134
|
-
console.log('
|
|
22135
|
-
console.log('
|
|
22136
|
-
console.log('
|
|
23182
|
+
this.textarea.style.hyphens = 'none';
|
|
23183
|
+
|
|
23184
|
+
// DEBUG: Log final CSS properties
|
|
23185
|
+
console.log('🎨 FINAL TEXTAREA CSS:');
|
|
23186
|
+
console.log(' textAlign:', this.textarea.style.textAlign);
|
|
23187
|
+
console.log(' textAlignLast:', this.textarea.style.textAlignLast);
|
|
23188
|
+
console.log(' direction:', this.textarea.style.direction);
|
|
23189
|
+
console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
|
|
23190
|
+
console.log(' width:', this.textarea.style.width);
|
|
23191
|
+
console.log(' textJustify:', this.textarea.style.textJustify);
|
|
23192
|
+
console.log(' wordSpacing:', this.textarea.style.wordSpacing);
|
|
23193
|
+
console.log(' whiteSpace:', this.textarea.style.whiteSpace);
|
|
23194
|
+
|
|
23195
|
+
// If justify, log Fabric object dimensions for comparison
|
|
23196
|
+
if (textAlign.includes('justify')) {
|
|
23197
|
+
var _calcTextWidth, _ref;
|
|
23198
|
+
console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
|
|
23199
|
+
console.log(' Fabric width:', target.width);
|
|
23200
|
+
console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
|
|
23201
|
+
console.log(' Fabric textAlign:', target.textAlign);
|
|
23202
|
+
console.log(' Text lines:', target.textLines);
|
|
23203
|
+
}
|
|
23204
|
+
|
|
23205
|
+
// Debug font properties matching
|
|
23206
|
+
console.log('🔤 FONT PROPERTIES COMPARISON:');
|
|
23207
|
+
console.log(' Fabric fontFamily:', target.fontFamily);
|
|
23208
|
+
console.log(' Fabric fontWeight:', target.fontWeight);
|
|
23209
|
+
console.log(' Fabric fontStyle:', target.fontStyle);
|
|
23210
|
+
console.log(' Fabric fontSize:', target.fontSize);
|
|
23211
|
+
console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
|
|
23212
|
+
console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
|
|
23213
|
+
console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
|
|
23214
|
+
console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
|
|
23215
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23216
|
+
|
|
23217
|
+
// Enhanced font rendering to better match fabric.js canvas rendering
|
|
23218
|
+
// Default to auto for more natural rendering
|
|
23219
|
+
this.textarea.style.webkitFontSmoothing = 'auto';
|
|
23220
|
+
this.textarea.style.mozOsxFontSmoothing = 'auto';
|
|
23221
|
+
this.textarea.style.fontSmooth = 'auto';
|
|
23222
|
+
this.textarea.style.textSizeAdjust = 'none';
|
|
23223
|
+
|
|
23224
|
+
// For bold fonts, use subpixel rendering to match canvas thickness better
|
|
23225
|
+
const fontWeight = String(target.fontWeight || 'normal');
|
|
23226
|
+
const isBold = fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 600;
|
|
23227
|
+
if (isBold) {
|
|
23228
|
+
this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
|
|
23229
|
+
this.textarea.style.mozOsxFontSmoothing = 'unset';
|
|
23230
|
+
console.log('🔤 Applied enhanced bold rendering for better thickness matching');
|
|
23231
|
+
}
|
|
23232
|
+
console.log('🎨 FONT SMOOTHING APPLIED:');
|
|
23233
|
+
console.log(' webkitFontSmoothing:', this.textarea.style.webkitFontSmoothing);
|
|
23234
|
+
console.log(' mozOsxFontSmoothing:', this.textarea.style.mozOsxFontSmoothing);
|
|
22137
23235
|
|
|
22138
23236
|
// Initial bounds are set correctly by Fabric.js - don't force update here
|
|
22139
23237
|
}
|
|
22140
23238
|
|
|
23239
|
+
/**
|
|
23240
|
+
* Debug method to compare textarea and canvas object bounding boxes
|
|
23241
|
+
*/
|
|
23242
|
+
debugBoundingBoxComparison() {
|
|
23243
|
+
const target = this.target;
|
|
23244
|
+
const canvas = this.canvas;
|
|
23245
|
+
const zoom = canvas.getZoom();
|
|
23246
|
+
|
|
23247
|
+
// Get textarea bounding box (in screen coordinates)
|
|
23248
|
+
const textareaRect = this.textarea.getBoundingClientRect();
|
|
23249
|
+
const hostRect = this.hostDiv.getBoundingClientRect();
|
|
23250
|
+
|
|
23251
|
+
// Get canvas object bounding box (in screen coordinates)
|
|
23252
|
+
const canvasBounds = target.getBoundingRect();
|
|
23253
|
+
const canvasRect = canvas.upperCanvasEl.getBoundingClientRect();
|
|
23254
|
+
|
|
23255
|
+
// Convert canvas object bounds to screen coordinates
|
|
23256
|
+
const vpt = canvas.viewportTransform;
|
|
23257
|
+
const screenObjectBounds = {
|
|
23258
|
+
left: canvasRect.left + canvasBounds.left * zoom + vpt[4],
|
|
23259
|
+
top: canvasRect.top + canvasBounds.top * zoom + vpt[5],
|
|
23260
|
+
width: canvasBounds.width * zoom,
|
|
23261
|
+
height: canvasBounds.height * zoom
|
|
23262
|
+
};
|
|
23263
|
+
console.log('🔍 BOUNDING BOX COMPARISON:');
|
|
23264
|
+
console.log('📦 Textarea Rect:', {
|
|
23265
|
+
left: Math.round(textareaRect.left * 100) / 100,
|
|
23266
|
+
top: Math.round(textareaRect.top * 100) / 100,
|
|
23267
|
+
width: Math.round(textareaRect.width * 100) / 100,
|
|
23268
|
+
height: Math.round(textareaRect.height * 100) / 100
|
|
23269
|
+
});
|
|
23270
|
+
console.log('📦 Host Div Rect:', {
|
|
23271
|
+
left: Math.round(hostRect.left * 100) / 100,
|
|
23272
|
+
top: Math.round(hostRect.top * 100) / 100,
|
|
23273
|
+
width: Math.round(hostRect.width * 100) / 100,
|
|
23274
|
+
height: Math.round(hostRect.height * 100) / 100
|
|
23275
|
+
});
|
|
23276
|
+
console.log('📦 Canvas Object Bounds (screen):', {
|
|
23277
|
+
left: Math.round(screenObjectBounds.left * 100) / 100,
|
|
23278
|
+
top: Math.round(screenObjectBounds.top * 100) / 100,
|
|
23279
|
+
width: Math.round(screenObjectBounds.width * 100) / 100,
|
|
23280
|
+
height: Math.round(screenObjectBounds.height * 100) / 100
|
|
23281
|
+
});
|
|
23282
|
+
console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
|
|
23283
|
+
|
|
23284
|
+
// Calculate differences
|
|
23285
|
+
const hostVsObject = {
|
|
23286
|
+
leftDiff: Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
|
|
23287
|
+
topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
|
|
23288
|
+
widthDiff: Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
|
|
23289
|
+
heightDiff: Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100
|
|
23290
|
+
};
|
|
23291
|
+
const textareaVsObject = {
|
|
23292
|
+
leftDiff: Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
|
|
23293
|
+
topDiff: Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
|
|
23294
|
+
widthDiff: Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
|
|
23295
|
+
heightDiff: Math.round((textareaRect.height - screenObjectBounds.height) * 100) / 100
|
|
23296
|
+
};
|
|
23297
|
+
console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
|
|
23298
|
+
console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
|
|
23299
|
+
|
|
23300
|
+
// Check if they're aligned (within 2px tolerance)
|
|
23301
|
+
const tolerance = 2;
|
|
23302
|
+
const hostAligned = Math.abs(hostVsObject.leftDiff) < tolerance && Math.abs(hostVsObject.topDiff) < tolerance && Math.abs(hostVsObject.widthDiff) < tolerance && Math.abs(hostVsObject.heightDiff) < tolerance;
|
|
23303
|
+
const textareaAligned = Math.abs(textareaVsObject.leftDiff) < tolerance && Math.abs(textareaVsObject.topDiff) < tolerance && Math.abs(textareaVsObject.widthDiff) < tolerance && Math.abs(textareaVsObject.heightDiff) < tolerance;
|
|
23304
|
+
console.log(hostAligned ? '✅ Host Div ALIGNED with canvas object' : '❌ Host Div MISALIGNED with canvas object');
|
|
23305
|
+
console.log(textareaAligned ? '✅ Textarea ALIGNED with canvas object' : '❌ Textarea MISALIGNED with canvas object');
|
|
23306
|
+
console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
|
|
23307
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23308
|
+
}
|
|
23309
|
+
|
|
23310
|
+
/**
|
|
23311
|
+
* Debug method to compare text wrapping between textarea and Fabric text object
|
|
23312
|
+
*/
|
|
23313
|
+
debugTextWrapping() {
|
|
23314
|
+
const target = this.target;
|
|
23315
|
+
const text = this.textarea.value;
|
|
23316
|
+
console.log('📝 TEXT WRAPPING COMPARISON:');
|
|
23317
|
+
console.log('📄 Text Content:', `"${text}"`);
|
|
23318
|
+
console.log('📄 Text Length:', text.length);
|
|
23319
|
+
|
|
23320
|
+
// Analyze line breaks
|
|
23321
|
+
const explicitLines = text.split('\n');
|
|
23322
|
+
console.log('📄 Explicit Lines (\\n):', explicitLines.length);
|
|
23323
|
+
explicitLines.forEach((line, i) => {
|
|
23324
|
+
console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
|
|
23325
|
+
});
|
|
23326
|
+
|
|
23327
|
+
// Get textarea computed styles for wrapping analysis
|
|
23328
|
+
const textareaStyles = window.getComputedStyle(this.textarea);
|
|
23329
|
+
console.log('📐 Textarea Wrapping Styles:');
|
|
23330
|
+
console.log(' width:', textareaStyles.width);
|
|
23331
|
+
console.log(' fontSize:', textareaStyles.fontSize);
|
|
23332
|
+
console.log(' fontFamily:', textareaStyles.fontFamily);
|
|
23333
|
+
console.log(' fontWeight:', textareaStyles.fontWeight);
|
|
23334
|
+
console.log(' letterSpacing:', textareaStyles.letterSpacing);
|
|
23335
|
+
console.log(' lineHeight:', textareaStyles.lineHeight);
|
|
23336
|
+
console.log(' whiteSpace:', textareaStyles.whiteSpace);
|
|
23337
|
+
console.log(' wordWrap:', textareaStyles.wordWrap);
|
|
23338
|
+
console.log(' overflowWrap:', textareaStyles.overflowWrap);
|
|
23339
|
+
console.log(' direction:', textareaStyles.direction);
|
|
23340
|
+
console.log(' textAlign:', textareaStyles.textAlign);
|
|
23341
|
+
|
|
23342
|
+
// Get Fabric text object properties for comparison
|
|
23343
|
+
console.log('📐 Fabric Text Object Properties:');
|
|
23344
|
+
console.log(' width:', target.width);
|
|
23345
|
+
console.log(' fontSize:', target.fontSize);
|
|
23346
|
+
console.log(' fontFamily:', target.fontFamily);
|
|
23347
|
+
console.log(' fontWeight:', target.fontWeight);
|
|
23348
|
+
console.log(' charSpacing:', target.charSpacing);
|
|
23349
|
+
console.log(' lineHeight:', target.lineHeight);
|
|
23350
|
+
console.log(' direction:', target.direction);
|
|
23351
|
+
console.log(' textAlign:', target.textAlign);
|
|
23352
|
+
console.log(' scaleX:', target.scaleX);
|
|
23353
|
+
console.log(' scaleY:', target.scaleY);
|
|
23354
|
+
|
|
23355
|
+
// Calculate effective dimensions for comparison - use actual rendered width
|
|
23356
|
+
// **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
|
|
23357
|
+
const fabricEffectiveWidth = this.target.getBoundingRect().width;
|
|
23358
|
+
// Use the exact width set on textarea for comparison
|
|
23359
|
+
const textareaComputedWidth = parseFloat(window.getComputedStyle(this.textarea).width);
|
|
23360
|
+
const textareaEffectiveWidth = textareaComputedWidth / this.canvas.getZoom();
|
|
23361
|
+
const widthDiff = Math.abs(textareaEffectiveWidth - fabricEffectiveWidth);
|
|
23362
|
+
console.log('📏 Effective Width Comparison:');
|
|
23363
|
+
console.log(' Textarea Effective Width:', textareaEffectiveWidth);
|
|
23364
|
+
console.log(' Fabric Effective Width:', fabricEffectiveWidth);
|
|
23365
|
+
console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
|
|
23366
|
+
console.log(widthDiff < 1 ? '✅ Widths MATCH for wrapping' : '❌ Width MISMATCH may cause different wrapping');
|
|
23367
|
+
|
|
23368
|
+
// Check text direction and bidi handling
|
|
23369
|
+
const hasRTLText = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
23370
|
+
const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
|
|
23371
|
+
console.log('🌍 Text Direction Analysis:');
|
|
23372
|
+
console.log(' Has RTL characters:', hasRTLText);
|
|
23373
|
+
console.log(' Has mixed Bidi text:', hasBidiText);
|
|
23374
|
+
console.log(' Textarea direction:', textareaStyles.direction);
|
|
23375
|
+
console.log(' Fabric direction:', target.direction || 'auto');
|
|
23376
|
+
console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
|
|
23377
|
+
|
|
23378
|
+
// Measure actual rendered line count
|
|
23379
|
+
const textareaScrollHeight = this.textarea.scrollHeight;
|
|
23380
|
+
const textareaLineHeight = parseFloat(textareaStyles.lineHeight) || parseFloat(textareaStyles.fontSize) * 1.2;
|
|
23381
|
+
const estimatedTextareaLines = Math.round(textareaScrollHeight / textareaLineHeight);
|
|
23382
|
+
console.log('📊 Line Count Analysis:');
|
|
23383
|
+
console.log(' Textarea scrollHeight:', textareaScrollHeight);
|
|
23384
|
+
console.log(' Textarea lineHeight:', textareaLineHeight);
|
|
23385
|
+
console.log(' Estimated rendered lines:', estimatedTextareaLines);
|
|
23386
|
+
console.log(' Explicit line breaks:', explicitLines.length);
|
|
23387
|
+
if (estimatedTextareaLines > explicitLines.length) {
|
|
23388
|
+
console.log('🔄 Text wrapping detected in textarea');
|
|
23389
|
+
console.log(' Wrapped lines:', estimatedTextareaLines - explicitLines.length);
|
|
23390
|
+
} else {
|
|
23391
|
+
console.log('📏 No text wrapping in textarea');
|
|
23392
|
+
}
|
|
23393
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23394
|
+
}
|
|
23395
|
+
|
|
22141
23396
|
/**
|
|
22142
23397
|
* Focus the textarea and position cursor at end
|
|
22143
23398
|
*/
|
|
@@ -22166,6 +23421,11 @@
|
|
|
22166
23421
|
this.canvas.requestRenderAll();
|
|
22167
23422
|
this.target.setCoords();
|
|
22168
23423
|
this.applyOverlayStyle();
|
|
23424
|
+
|
|
23425
|
+
// Fix character mapping issues after JSON loading for browser-wrapped fonts
|
|
23426
|
+
if (this.target._fixCharacterMappingAfterJsonLoad) {
|
|
23427
|
+
this.target._fixCharacterMappingAfterJsonLoad();
|
|
23428
|
+
}
|
|
22169
23429
|
this.textarea.focus();
|
|
22170
23430
|
this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
|
|
22171
23431
|
|
|
@@ -22211,6 +23471,23 @@
|
|
|
22211
23471
|
// Handle commit/cancel after restoring visibility
|
|
22212
23472
|
if (commit && !this.isComposing) {
|
|
22213
23473
|
const finalText = this.textarea.value;
|
|
23474
|
+
|
|
23475
|
+
// Auto-detect text direction and update fabric object if needed
|
|
23476
|
+
const detectedDirection = this.firstStrongDir(finalText);
|
|
23477
|
+
const currentDirection = this.target.direction || 'ltr';
|
|
23478
|
+
if (detectedDirection && detectedDirection !== currentDirection) {
|
|
23479
|
+
console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
|
|
23480
|
+
console.log(` Text content: "${finalText.substring(0, 50)}..."`);
|
|
23481
|
+
|
|
23482
|
+
// Update the fabric object's direction
|
|
23483
|
+
this.target.set('direction', detectedDirection);
|
|
23484
|
+
|
|
23485
|
+
// Force a re-render to apply the direction change
|
|
23486
|
+
this.canvas.requestRenderAll();
|
|
23487
|
+
console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
|
|
23488
|
+
} else {
|
|
23489
|
+
console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
|
|
23490
|
+
}
|
|
22214
23491
|
if (this.onCommit) {
|
|
22215
23492
|
this.onCommit(finalText);
|
|
22216
23493
|
}
|
|
@@ -22250,25 +23527,40 @@
|
|
|
22250
23527
|
}
|
|
22251
23528
|
}
|
|
22252
23529
|
autoResizeTextarea() {
|
|
22253
|
-
//
|
|
22254
|
-
const
|
|
22255
|
-
|
|
22256
|
-
|
|
22257
|
-
|
|
23530
|
+
// Store the scroll position and the container's old height for comparison.
|
|
23531
|
+
const scrollTop = this.textarea.scrollTop;
|
|
23532
|
+
const oldHeight = parseFloat(this.hostDiv.style.height || '0');
|
|
23533
|
+
|
|
23534
|
+
// 1. **Force a reliable reflow.**
|
|
23535
|
+
// First, reset the textarea's height to a minimal value. This is the crucial step
|
|
23536
|
+
// that forces the browser to recalculate the content's height from scratch,
|
|
23537
|
+
// ignoring the hostDiv's larger, stale height.
|
|
23538
|
+
this.textarea.style.height = '1px';
|
|
23539
|
+
|
|
23540
|
+
// 2. Read the now-accurate scrollHeight. This value reflects the minimum
|
|
23541
|
+
// height required for the content, whether it's single or multi-line.
|
|
22258
23542
|
const scrollHeight = this.textarea.scrollHeight;
|
|
22259
23543
|
|
|
22260
|
-
//
|
|
22261
|
-
const
|
|
22262
|
-
const newHeight =
|
|
22263
|
-
const heightChanged = Math.abs(newHeight - oldHeight) > 2; // Only if meaningful change
|
|
23544
|
+
// A small buffer for rendering consistency across browsers.
|
|
23545
|
+
const buffer = 2;
|
|
23546
|
+
const newHeight = scrollHeight + buffer;
|
|
22264
23547
|
|
|
22265
|
-
|
|
22266
|
-
|
|
23548
|
+
// Check if the height has changed significantly.
|
|
23549
|
+
const heightChanged = Math.abs(newHeight - oldHeight) > 1;
|
|
22267
23550
|
|
|
22268
|
-
// Only update object bounds if
|
|
23551
|
+
// 4. Only update heights and object bounds if there was a change.
|
|
22269
23552
|
if (heightChanged) {
|
|
23553
|
+
this.textarea.style.height = `${newHeight}px`;
|
|
23554
|
+
this.hostDiv.style.height = `${newHeight}px`;
|
|
22270
23555
|
this.updateObjectBounds();
|
|
23556
|
+
} else {
|
|
23557
|
+
// If no significant change, ensure the textarea's height matches the container
|
|
23558
|
+
// to prevent any minor visual misalignment.
|
|
23559
|
+
this.textarea.style.height = this.hostDiv.style.height;
|
|
22271
23560
|
}
|
|
23561
|
+
|
|
23562
|
+
// 5. Restore the original scroll position.
|
|
23563
|
+
this.textarea.scrollTop = scrollTop;
|
|
22272
23564
|
}
|
|
22273
23565
|
handleKeyDown(e) {
|
|
22274
23566
|
if (e.key === 'Escape') {
|
|
@@ -22277,6 +23569,19 @@
|
|
|
22277
23569
|
} else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
22278
23570
|
e.preventDefault();
|
|
22279
23571
|
this.destroy(true); // Commit
|
|
23572
|
+
} else if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
|
|
23573
|
+
// For keys that might change the height, schedule a resize check
|
|
23574
|
+
// Use both immediate and delayed checks to catch all scenarios
|
|
23575
|
+
requestAnimationFrame(() => {
|
|
23576
|
+
if (!this.isDestroyed) {
|
|
23577
|
+
this.autoResizeTextarea();
|
|
23578
|
+
}
|
|
23579
|
+
});
|
|
23580
|
+
setTimeout(() => {
|
|
23581
|
+
if (!this.isDestroyed) {
|
|
23582
|
+
this.autoResizeTextarea();
|
|
23583
|
+
}
|
|
23584
|
+
}, 10); // Small delay to ensure DOM is updated
|
|
22280
23585
|
}
|
|
22281
23586
|
}
|
|
22282
23587
|
handleFocus() {
|
|
@@ -25253,6 +26558,7 @@
|
|
|
25253
26558
|
...Textbox.ownDefaults,
|
|
25254
26559
|
...options
|
|
25255
26560
|
});
|
|
26561
|
+
this.initializeEventListeners();
|
|
25256
26562
|
}
|
|
25257
26563
|
|
|
25258
26564
|
/**
|
|
@@ -25274,8 +26580,27 @@
|
|
|
25274
26580
|
*/
|
|
25275
26581
|
initDimensions() {
|
|
25276
26582
|
if (!this.initialized) {
|
|
26583
|
+
this.initialized = true;
|
|
26584
|
+
}
|
|
26585
|
+
|
|
26586
|
+
// Prevent rapid recalculations during moves
|
|
26587
|
+
if (this._usingBrowserWrapping) {
|
|
26588
|
+
const now = Date.now();
|
|
26589
|
+
const lastCall = this._lastInitDimensionsTime || 0;
|
|
26590
|
+
const isRapidCall = now - lastCall < 100;
|
|
26591
|
+
const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
|
|
26592
|
+
if (isRapidCall && !isDuringLoading) {
|
|
26593
|
+
return;
|
|
26594
|
+
}
|
|
26595
|
+
this._lastInitDimensionsTime = now;
|
|
26596
|
+
}
|
|
26597
|
+
|
|
26598
|
+
// Skip if nothing changed
|
|
26599
|
+
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
26600
|
+
if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
|
|
25277
26601
|
return;
|
|
25278
26602
|
}
|
|
26603
|
+
this._lastDimensionState = currentState;
|
|
25279
26604
|
|
|
25280
26605
|
// Use advanced layout if enabled
|
|
25281
26606
|
if (this.enableAdvancedLayout) {
|
|
@@ -25286,17 +26611,142 @@
|
|
|
25286
26611
|
// clear dynamicMinWidth as it will be different after we re-wrap line
|
|
25287
26612
|
this.dynamicMinWidth = 0;
|
|
25288
26613
|
// wrap lines
|
|
25289
|
-
|
|
25290
|
-
|
|
25291
|
-
|
|
26614
|
+
const splitTextResult = this._splitText();
|
|
26615
|
+
this._styleMap = this._generateStyleMap(splitTextResult);
|
|
26616
|
+
|
|
26617
|
+
// For browser wrapping, ensure _textLines is set from browser results
|
|
26618
|
+
if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
|
|
26619
|
+
this._textLines = splitTextResult.lines.map(line => line.split(''));
|
|
26620
|
+
|
|
26621
|
+
// Store justify measurements and browser height
|
|
26622
|
+
const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
|
|
26623
|
+
if (justifyMeasurements) {
|
|
26624
|
+
this._styleMap.justifySpaceMeasurements = justifyMeasurements;
|
|
26625
|
+
}
|
|
26626
|
+
const actualHeight = splitTextResult.actualBrowserHeight;
|
|
26627
|
+
if (actualHeight) {
|
|
26628
|
+
this._actualBrowserHeight = actualHeight;
|
|
26629
|
+
}
|
|
26630
|
+
}
|
|
26631
|
+
// Don't auto-resize width when using browser wrapping to prevent width increases during moves
|
|
26632
|
+
if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
|
|
25292
26633
|
this._set('width', this.dynamicMinWidth);
|
|
25293
26634
|
}
|
|
26635
|
+
|
|
26636
|
+
// For browser wrapping fonts (like STV), ensure minimum width for new textboxes
|
|
26637
|
+
// since these fonts can't measure English characters properly
|
|
26638
|
+
if (this._usingBrowserWrapping && this.width < 50) {
|
|
26639
|
+
console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
|
|
26640
|
+
this.width = 300;
|
|
26641
|
+
}
|
|
26642
|
+
|
|
26643
|
+
// Mark browser wrapping as initialized when complete
|
|
26644
|
+
if (this._usingBrowserWrapping) {
|
|
26645
|
+
this._browserWrapInitialized = true;
|
|
26646
|
+
}
|
|
25294
26647
|
if (this.textAlign.includes(JUSTIFY)) {
|
|
26648
|
+
// For browser wrapping fonts, apply browser-calculated justify spaces
|
|
26649
|
+
if (this._usingBrowserWrapping) {
|
|
26650
|
+
console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
|
|
26651
|
+
this._applyBrowserJustifySpaces();
|
|
26652
|
+
return;
|
|
26653
|
+
}
|
|
26654
|
+
|
|
26655
|
+
// Don't apply justify alignment during drag operations to prevent snapping
|
|
26656
|
+
const now = Date.now();
|
|
26657
|
+
const lastDragTime = this._lastInitDimensionsTime || 0;
|
|
26658
|
+
const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
|
|
26659
|
+
|
|
26660
|
+
if (isDuringDrag) {
|
|
26661
|
+
console.log('🔤 Skipping justify during drag operation to prevent snapping');
|
|
26662
|
+
return;
|
|
26663
|
+
}
|
|
26664
|
+
|
|
26665
|
+
// For non-browser-wrapping fonts, use Fabric's justify system
|
|
25295
26666
|
// once text is measured we need to make space fatter to make justified text.
|
|
25296
|
-
|
|
26667
|
+
// Ensure __charBounds exists and fonts are ready before applying justify
|
|
26668
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
26669
|
+
// Check if font is ready for accurate justify calculations
|
|
26670
|
+
const fontReady = this._isFontReady ? this._isFontReady() : true;
|
|
26671
|
+
if (fontReady) {
|
|
26672
|
+
this.enlargeSpaces();
|
|
26673
|
+
} else {
|
|
26674
|
+
console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
|
|
26675
|
+
// Defer justify calculation until font is ready
|
|
26676
|
+
this._scheduleJustifyAfterFontLoad();
|
|
26677
|
+
}
|
|
26678
|
+
} else {
|
|
26679
|
+
console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
|
|
26680
|
+
// Defer the justify calculation until the next frame
|
|
26681
|
+
setTimeout(() => {
|
|
26682
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
26683
|
+
var _this$canvas;
|
|
26684
|
+
console.log('🔧 Applying deferred Textbox justify alignment');
|
|
26685
|
+
this.enlargeSpaces();
|
|
26686
|
+
(_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
|
|
26687
|
+
}
|
|
26688
|
+
}, 0);
|
|
26689
|
+
}
|
|
26690
|
+
}
|
|
26691
|
+
// Calculate height - use Fabric's calculation for proper text rendering space
|
|
26692
|
+
if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
|
|
26693
|
+
const actualBrowserHeight = this._actualBrowserHeight;
|
|
26694
|
+
const oldHeight = this.height;
|
|
26695
|
+
// Use Fabric's height calculation since it knows how much space text rendering needs
|
|
26696
|
+
this.height = this.calcTextHeight();
|
|
26697
|
+
|
|
26698
|
+
// Force canvas refresh and control update if height changed significantly
|
|
26699
|
+
if (Math.abs(this.height - oldHeight) > 1) {
|
|
26700
|
+
var _this$canvas2, _this$_textLines;
|
|
26701
|
+
this.setCoords();
|
|
26702
|
+
(_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
|
|
26703
|
+
|
|
26704
|
+
// DEBUG: Log exact positioning details
|
|
26705
|
+
console.log(`🎯 POSITIONING DEBUG:`);
|
|
26706
|
+
console.log(` Textbox height: ${this.height}px`);
|
|
26707
|
+
console.log(` Textbox top: ${this.top}px`);
|
|
26708
|
+
console.log(` Textbox left: ${this.left}px`);
|
|
26709
|
+
console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
|
|
26710
|
+
console.log(` Font size: ${this.fontSize}px`);
|
|
26711
|
+
console.log(` Line height: ${this.lineHeight || 1.16}`);
|
|
26712
|
+
console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
|
|
26713
|
+
console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
|
|
26714
|
+
console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
|
|
26715
|
+
console.log(` Browser height: ${actualBrowserHeight}px`);
|
|
26716
|
+
console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
|
|
26717
|
+
}
|
|
26718
|
+
} else {
|
|
26719
|
+
this.height = this.calcTextHeight();
|
|
26720
|
+
}
|
|
26721
|
+
}
|
|
26722
|
+
|
|
26723
|
+
/**
|
|
26724
|
+
* Schedule justify calculation after font loads (Textbox-specific)
|
|
26725
|
+
* @private
|
|
26726
|
+
*/
|
|
26727
|
+
_scheduleJustifyAfterFontLoad() {
|
|
26728
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
26729
|
+
return;
|
|
25297
26730
|
}
|
|
25298
|
-
|
|
25299
|
-
|
|
26731
|
+
|
|
26732
|
+
// Only schedule if not already waiting
|
|
26733
|
+
if (this._fontJustifyScheduled) {
|
|
26734
|
+
return;
|
|
26735
|
+
}
|
|
26736
|
+
this._fontJustifyScheduled = true;
|
|
26737
|
+
const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
|
|
26738
|
+
document.fonts.load(fontSpec).then(() => {
|
|
26739
|
+
var _this$canvas3;
|
|
26740
|
+
this._fontJustifyScheduled = false;
|
|
26741
|
+
console.log('🔧 Textbox: Font loaded, applying justify alignment');
|
|
26742
|
+
|
|
26743
|
+
// Re-run initDimensions to ensure proper justify calculation
|
|
26744
|
+
this.initDimensions();
|
|
26745
|
+
(_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
|
|
26746
|
+
}).catch(() => {
|
|
26747
|
+
this._fontJustifyScheduled = false;
|
|
26748
|
+
console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
|
|
26749
|
+
});
|
|
25300
26750
|
}
|
|
25301
26751
|
|
|
25302
26752
|
/**
|
|
@@ -25663,19 +27113,33 @@
|
|
|
25663
27113
|
width: wordWidth
|
|
25664
27114
|
} = data[i];
|
|
25665
27115
|
offset += word.length;
|
|
25666
|
-
|
|
25667
|
-
if
|
|
27116
|
+
|
|
27117
|
+
// Predictive wrapping: check if adding this word would exceed the width
|
|
27118
|
+
const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
|
|
27119
|
+
// Use exact width to match overlay editor behavior
|
|
27120
|
+
const conservativeMaxWidth = maxWidth; // No artificial buffer
|
|
27121
|
+
|
|
27122
|
+
// Debug logging for wrapping decisions
|
|
27123
|
+
const currentLineText = line.join('');
|
|
27124
|
+
console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
|
|
27125
|
+
if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
|
|
27126
|
+
// This word would exceed the width, wrap before adding it
|
|
27127
|
+
console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
|
|
25668
27128
|
graphemeLines.push(line);
|
|
25669
27129
|
line = [];
|
|
25670
|
-
lineWidth = wordWidth;
|
|
27130
|
+
lineWidth = wordWidth; // Start new line with just this word
|
|
25671
27131
|
lineJustStarted = true;
|
|
25672
27132
|
} else {
|
|
25673
|
-
|
|
27133
|
+
// Word fits, add it to current line
|
|
27134
|
+
lineWidth = potentialLineWidth + additionalSpace;
|
|
25674
27135
|
}
|
|
25675
27136
|
if (!lineJustStarted && !splitByGrapheme) {
|
|
25676
27137
|
line.push(infix);
|
|
25677
27138
|
}
|
|
25678
27139
|
line = line.concat(word);
|
|
27140
|
+
|
|
27141
|
+
// Debug: show current line after adding word
|
|
27142
|
+
console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
|
|
25679
27143
|
infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
|
|
25680
27144
|
offset++;
|
|
25681
27145
|
lineJustStarted = false;
|
|
@@ -25685,9 +27149,19 @@
|
|
|
25685
27149
|
// TODO: this code is probably not necessary anymore.
|
|
25686
27150
|
// it can be moved out of this function since largestWordWidth is now
|
|
25687
27151
|
// known in advance
|
|
25688
|
-
|
|
27152
|
+
// Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
|
|
27153
|
+
if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
|
|
27154
|
+
console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
|
|
25689
27155
|
this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
|
|
27156
|
+
} else if (this._usingBrowserWrapping) {
|
|
27157
|
+
console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
|
|
25690
27158
|
}
|
|
27159
|
+
|
|
27160
|
+
// Debug: show final wrapped lines
|
|
27161
|
+
console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
|
|
27162
|
+
graphemeLines.forEach((line, i) => {
|
|
27163
|
+
console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
|
|
27164
|
+
});
|
|
25691
27165
|
return graphemeLines;
|
|
25692
27166
|
}
|
|
25693
27167
|
|
|
@@ -25731,6 +27205,260 @@
|
|
|
25731
27205
|
* @override
|
|
25732
27206
|
*/
|
|
25733
27207
|
_splitTextIntoLines(text) {
|
|
27208
|
+
// Check if we need browser wrapping using smart font detection
|
|
27209
|
+
const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
|
|
27210
|
+
if (needsBrowserWrapping) {
|
|
27211
|
+
// Cache key based on text content, width, font properties, AND text alignment
|
|
27212
|
+
const textHash = text.length + text.slice(0, 50); // Include text content in cache key
|
|
27213
|
+
const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
27214
|
+
|
|
27215
|
+
// Check if we have a cached result and nothing has changed
|
|
27216
|
+
if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
|
|
27217
|
+
const cachedResult = this._browserWrapCache.result;
|
|
27218
|
+
|
|
27219
|
+
// For justify alignment, ensure we have the measurements
|
|
27220
|
+
if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
|
|
27221
|
+
return cachedResult;
|
|
27222
|
+
}
|
|
27223
|
+
}
|
|
27224
|
+
const result = this._splitTextIntoLinesWithBrowser(text);
|
|
27225
|
+
|
|
27226
|
+
// Cache the result
|
|
27227
|
+
this._browserWrapCache = {
|
|
27228
|
+
key: cacheKey,
|
|
27229
|
+
result
|
|
27230
|
+
};
|
|
27231
|
+
|
|
27232
|
+
// Mark that we used browser wrapping to prevent dynamicMinWidth modifications
|
|
27233
|
+
this._usingBrowserWrapping = true;
|
|
27234
|
+
return result;
|
|
27235
|
+
}
|
|
27236
|
+
|
|
27237
|
+
// Clear the browser wrapping flag when using regular wrapping
|
|
27238
|
+
this._usingBrowserWrapping = false;
|
|
27239
|
+
|
|
27240
|
+
// Default Fabric wrapping for other fonts
|
|
27241
|
+
const newText = super._splitTextIntoLines(text),
|
|
27242
|
+
graphemeLines = this._wrapText(newText.lines, this.width),
|
|
27243
|
+
lines = new Array(graphemeLines.length);
|
|
27244
|
+
for (let i = 0; i < graphemeLines.length; i++) {
|
|
27245
|
+
lines[i] = graphemeLines[i].join('');
|
|
27246
|
+
}
|
|
27247
|
+
newText.lines = lines;
|
|
27248
|
+
newText.graphemeLines = graphemeLines;
|
|
27249
|
+
return newText;
|
|
27250
|
+
}
|
|
27251
|
+
|
|
27252
|
+
/**
|
|
27253
|
+
* Use browser's native text wrapping for accurate handling of fonts without English glyphs
|
|
27254
|
+
* @private
|
|
27255
|
+
*/
|
|
27256
|
+
_splitTextIntoLinesWithBrowser(text) {
|
|
27257
|
+
if (typeof document === 'undefined') {
|
|
27258
|
+
// Fallback to regular wrapping in Node.js
|
|
27259
|
+
return this._splitTextIntoLinesDefault(text);
|
|
27260
|
+
}
|
|
27261
|
+
|
|
27262
|
+
// Create a hidden element that mimics the overlay editor
|
|
27263
|
+
const testElement = document.createElement('div');
|
|
27264
|
+
testElement.style.position = 'absolute';
|
|
27265
|
+
testElement.style.left = '-9999px';
|
|
27266
|
+
testElement.style.visibility = 'hidden';
|
|
27267
|
+
testElement.style.fontSize = `${this.fontSize}px`;
|
|
27268
|
+
testElement.style.fontFamily = `"${this.fontFamily}"`;
|
|
27269
|
+
testElement.style.fontWeight = String(this.fontWeight || 'normal');
|
|
27270
|
+
testElement.style.fontStyle = String(this.fontStyle || 'normal');
|
|
27271
|
+
testElement.style.lineHeight = String(this.lineHeight || 1.16);
|
|
27272
|
+
testElement.style.width = `${this.width}px`;
|
|
27273
|
+
testElement.style.direction = this.direction || 'ltr';
|
|
27274
|
+
testElement.style.whiteSpace = 'pre-wrap';
|
|
27275
|
+
testElement.style.wordBreak = 'normal';
|
|
27276
|
+
testElement.style.overflowWrap = 'break-word';
|
|
27277
|
+
|
|
27278
|
+
// Set browser-native text alignment (including justify)
|
|
27279
|
+
if (this.textAlign.includes('justify')) {
|
|
27280
|
+
testElement.style.textAlign = 'justify';
|
|
27281
|
+
testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
|
|
27282
|
+
} else {
|
|
27283
|
+
testElement.style.textAlign = this.textAlign;
|
|
27284
|
+
}
|
|
27285
|
+
testElement.textContent = text;
|
|
27286
|
+
document.body.appendChild(testElement);
|
|
27287
|
+
|
|
27288
|
+
// Get the browser's natural line breaks
|
|
27289
|
+
const range = document.createRange();
|
|
27290
|
+
const lines = [];
|
|
27291
|
+
const graphemeLines = [];
|
|
27292
|
+
try {
|
|
27293
|
+
// Simple approach: split by measuring character positions
|
|
27294
|
+
const textNode = testElement.firstChild;
|
|
27295
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
27296
|
+
let currentLineStart = 0;
|
|
27297
|
+
const textLength = text.length;
|
|
27298
|
+
let previousBottom = 0;
|
|
27299
|
+
for (let i = 0; i <= textLength; i++) {
|
|
27300
|
+
range.setStart(textNode, currentLineStart);
|
|
27301
|
+
range.setEnd(textNode, i);
|
|
27302
|
+
const rect = range.getBoundingClientRect();
|
|
27303
|
+
if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
|
|
27304
|
+
// New line detected or end of text
|
|
27305
|
+
const lineEnd = i === textLength ? i : i - 1;
|
|
27306
|
+
const lineText = text.substring(currentLineStart, lineEnd).trim();
|
|
27307
|
+
if (lineText) {
|
|
27308
|
+
lines.push(lineText);
|
|
27309
|
+
// Convert to graphemes for compatibility
|
|
27310
|
+
const graphemeLine = lineText.split('');
|
|
27311
|
+
graphemeLines.push(graphemeLine);
|
|
27312
|
+
}
|
|
27313
|
+
currentLineStart = lineEnd;
|
|
27314
|
+
previousBottom = rect.bottom;
|
|
27315
|
+
}
|
|
27316
|
+
}
|
|
27317
|
+
}
|
|
27318
|
+
} catch (error) {
|
|
27319
|
+
console.warn('Browser wrapping failed, using fallback:', error);
|
|
27320
|
+
document.body.removeChild(testElement);
|
|
27321
|
+
return this._splitTextIntoLinesDefault(text);
|
|
27322
|
+
}
|
|
27323
|
+
|
|
27324
|
+
// Extract actual browser height BEFORE removing element
|
|
27325
|
+
const actualBrowserHeight = testElement.scrollHeight;
|
|
27326
|
+
const offsetHeight = testElement.offsetHeight;
|
|
27327
|
+
const clientHeight = testElement.clientHeight;
|
|
27328
|
+
const boundingRect = testElement.getBoundingClientRect();
|
|
27329
|
+
console.log(`🔤 Browser element measurements:`);
|
|
27330
|
+
console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
|
|
27331
|
+
console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
|
|
27332
|
+
console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
|
|
27333
|
+
console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
|
|
27334
|
+
console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
|
|
27335
|
+
|
|
27336
|
+
// For justify alignment, extract space measurements from browser BEFORE removing element
|
|
27337
|
+
let justifySpaceMeasurements = null;
|
|
27338
|
+
if (this.textAlign.includes('justify')) {
|
|
27339
|
+
justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
|
|
27340
|
+
}
|
|
27341
|
+
document.body.removeChild(testElement);
|
|
27342
|
+
console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
|
|
27343
|
+
|
|
27344
|
+
// Try different height measurements to find the most accurate
|
|
27345
|
+
let bestHeight = actualBrowserHeight;
|
|
27346
|
+
|
|
27347
|
+
// If scrollHeight and offsetHeight differ significantly, investigate
|
|
27348
|
+
if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
|
|
27349
|
+
console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
|
|
27350
|
+
}
|
|
27351
|
+
|
|
27352
|
+
// Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
|
|
27353
|
+
if (boundingRect.height > bestHeight) {
|
|
27354
|
+
console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
|
|
27355
|
+
bestHeight = boundingRect.height;
|
|
27356
|
+
}
|
|
27357
|
+
|
|
27358
|
+
// Font-specific height adjustments for accurate bounding box
|
|
27359
|
+
let adjustedHeight = bestHeight;
|
|
27360
|
+
|
|
27361
|
+
// Fonts without English glyphs need additional height buffer due to different font metrics
|
|
27362
|
+
const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
|
|
27363
|
+
if (lacksEnglishGlyphs) {
|
|
27364
|
+
const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
|
|
27365
|
+
adjustedHeight = bestHeight + glyphBuffer;
|
|
27366
|
+
console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
|
|
27367
|
+
} else {
|
|
27368
|
+
console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
|
|
27369
|
+
}
|
|
27370
|
+
return {
|
|
27371
|
+
_unwrappedLines: [text.split('')],
|
|
27372
|
+
lines: lines,
|
|
27373
|
+
graphemeText: text.split(''),
|
|
27374
|
+
graphemeLines: graphemeLines,
|
|
27375
|
+
justifySpaceMeasurements: justifySpaceMeasurements,
|
|
27376
|
+
actualBrowserHeight: adjustedHeight
|
|
27377
|
+
};
|
|
27378
|
+
}
|
|
27379
|
+
|
|
27380
|
+
/**
|
|
27381
|
+
* Extract justify space measurements from browser
|
|
27382
|
+
* @private
|
|
27383
|
+
*/
|
|
27384
|
+
_extractJustifySpaceMeasurements(element, lines) {
|
|
27385
|
+
console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
|
|
27386
|
+
|
|
27387
|
+
// For now, we'll use a simplified approach:
|
|
27388
|
+
// Apply uniform space expansion to match the line width
|
|
27389
|
+
const spaceWidths = [];
|
|
27390
|
+
lines.forEach((line, lineIndex) => {
|
|
27391
|
+
const lineSpaces = [];
|
|
27392
|
+
const spaceCount = (line.match(/\s/g) || []).length;
|
|
27393
|
+
if (spaceCount > 0 && lineIndex < lines.length - 1) {
|
|
27394
|
+
// Don't justify last line
|
|
27395
|
+
// Calculate how much space expansion is needed
|
|
27396
|
+
const normalSpaceWidth = 6.4; // Default space width for STV font
|
|
27397
|
+
const lineWidth = this.width;
|
|
27398
|
+
|
|
27399
|
+
// Estimate natural line width
|
|
27400
|
+
const charCount = line.length - spaceCount;
|
|
27401
|
+
const avgCharWidth = 12; // Approximate for STV font
|
|
27402
|
+
|
|
27403
|
+
// Calculate expanded space width
|
|
27404
|
+
const remainingSpace = lineWidth - charCount * avgCharWidth;
|
|
27405
|
+
const expandedSpaceWidth = remainingSpace / spaceCount;
|
|
27406
|
+
console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
|
|
27407
|
+
|
|
27408
|
+
// Fill array with expanded space widths for this line
|
|
27409
|
+
for (let i = 0; i < spaceCount; i++) {
|
|
27410
|
+
lineSpaces.push(expandedSpaceWidth);
|
|
27411
|
+
}
|
|
27412
|
+
}
|
|
27413
|
+
spaceWidths.push(lineSpaces);
|
|
27414
|
+
});
|
|
27415
|
+
return spaceWidths;
|
|
27416
|
+
}
|
|
27417
|
+
|
|
27418
|
+
/**
|
|
27419
|
+
* Apply browser-calculated justify space measurements
|
|
27420
|
+
* @private
|
|
27421
|
+
*/
|
|
27422
|
+
_applyBrowserJustifySpaces() {
|
|
27423
|
+
if (!this._textLines || !this.__charBounds) {
|
|
27424
|
+
console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
|
|
27425
|
+
return;
|
|
27426
|
+
}
|
|
27427
|
+
|
|
27428
|
+
// Get space measurements from browser wrapping result
|
|
27429
|
+
const styleMap = this._styleMap;
|
|
27430
|
+
if (!styleMap || !styleMap.justifySpaceMeasurements) {
|
|
27431
|
+
console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
|
|
27432
|
+
return;
|
|
27433
|
+
}
|
|
27434
|
+
const spaceWidths = styleMap.justifySpaceMeasurements;
|
|
27435
|
+
console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
|
|
27436
|
+
|
|
27437
|
+
// Apply space widths to character bounds
|
|
27438
|
+
this._textLines.forEach((line, lineIndex) => {
|
|
27439
|
+
if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
|
|
27440
|
+
const lineBounds = this.__charBounds[lineIndex];
|
|
27441
|
+
const lineSpaceWidths = spaceWidths[lineIndex];
|
|
27442
|
+
let spaceIndex = 0;
|
|
27443
|
+
for (let charIndex = 0; charIndex < line.length; charIndex++) {
|
|
27444
|
+
if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
|
|
27445
|
+
const expandedWidth = lineSpaceWidths[spaceIndex];
|
|
27446
|
+
if (lineBounds[charIndex]) {
|
|
27447
|
+
const oldWidth = lineBounds[charIndex].width;
|
|
27448
|
+
lineBounds[charIndex].width = expandedWidth;
|
|
27449
|
+
console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
|
|
27450
|
+
}
|
|
27451
|
+
spaceIndex++;
|
|
27452
|
+
}
|
|
27453
|
+
}
|
|
27454
|
+
});
|
|
27455
|
+
}
|
|
27456
|
+
|
|
27457
|
+
/**
|
|
27458
|
+
* Fallback to default Fabric wrapping
|
|
27459
|
+
* @private
|
|
27460
|
+
*/
|
|
27461
|
+
_splitTextIntoLinesDefault(text) {
|
|
25734
27462
|
const newText = super._splitTextIntoLines(text),
|
|
25735
27463
|
graphemeLines = this._wrapText(newText.lines, this.width),
|
|
25736
27464
|
lines = new Array(graphemeLines.length);
|
|
@@ -25760,6 +27488,204 @@
|
|
|
25760
27488
|
}
|
|
25761
27489
|
}
|
|
25762
27490
|
|
|
27491
|
+
/**
|
|
27492
|
+
* Initialize event listeners for safety snap functionality
|
|
27493
|
+
* @private
|
|
27494
|
+
*/
|
|
27495
|
+
initializeEventListeners() {
|
|
27496
|
+
var _this$canvas4;
|
|
27497
|
+
// Track which side is being used for resize to handle position compensation
|
|
27498
|
+
let resizeOrigin = null;
|
|
27499
|
+
|
|
27500
|
+
// Detect resize origin during resizing
|
|
27501
|
+
this.on('resizing', e => {
|
|
27502
|
+
// Check transform origin to determine which side is being resized
|
|
27503
|
+
if (e.transform) {
|
|
27504
|
+
const {
|
|
27505
|
+
originX
|
|
27506
|
+
} = e.transform;
|
|
27507
|
+
// originX tells us which side is the anchor - opposite side is being dragged
|
|
27508
|
+
resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
|
|
27509
|
+
} else if (e.originX) {
|
|
27510
|
+
const {
|
|
27511
|
+
originX
|
|
27512
|
+
} = e;
|
|
27513
|
+
resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
|
|
27514
|
+
}
|
|
27515
|
+
});
|
|
27516
|
+
|
|
27517
|
+
// Only trigger safety snap after resize is complete (not during)
|
|
27518
|
+
// Use 'modified' event which fires after user releases the mouse
|
|
27519
|
+
this.on('modified', () => {
|
|
27520
|
+
const currentResizeOrigin = resizeOrigin; // Capture the value before reset
|
|
27521
|
+
// Small delay to ensure text layout is updated
|
|
27522
|
+
setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
|
|
27523
|
+
resizeOrigin = null; // Reset after capturing
|
|
27524
|
+
});
|
|
27525
|
+
|
|
27526
|
+
// Also listen to canvas-level modified event as backup
|
|
27527
|
+
(_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
|
|
27528
|
+
if (e.target === this) {
|
|
27529
|
+
const currentResizeOrigin = resizeOrigin; // Capture the value before reset
|
|
27530
|
+
setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
|
|
27531
|
+
resizeOrigin = null; // Reset after capturing
|
|
27532
|
+
}
|
|
27533
|
+
});
|
|
27534
|
+
}
|
|
27535
|
+
|
|
27536
|
+
/**
|
|
27537
|
+
* Safety snap to prevent glyph clipping after manual resize.
|
|
27538
|
+
* Similar to Polotno - checks if any glyphs are too close to edges
|
|
27539
|
+
* and automatically expands width if needed.
|
|
27540
|
+
* @private
|
|
27541
|
+
* @param resizeOrigin - Which side was used for resizing ('left' or 'right')
|
|
27542
|
+
*/
|
|
27543
|
+
safetySnapWidth(resizeOrigin) {
|
|
27544
|
+
// For Textbox objects, we always want to check for clipping regardless of isWrapping flag
|
|
27545
|
+
if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
|
|
27546
|
+
return;
|
|
27547
|
+
}
|
|
27548
|
+
const lineCount = this._textLines.length;
|
|
27549
|
+
if (lineCount === 0) return;
|
|
27550
|
+
let maxRequiredWidth = 0; // Width including RTL buffer
|
|
27551
|
+
|
|
27552
|
+
for (let i = 0; i < lineCount; i++) {
|
|
27553
|
+
const lineText = this._textLines[i].join(''); // Convert grapheme array to string
|
|
27554
|
+
const lineWidth = this.getLineWidth(i);
|
|
27555
|
+
|
|
27556
|
+
// RTL detection - regex for Arabic, Hebrew, and other RTL characters
|
|
27557
|
+
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
|
27558
|
+
if (rtlRegex.test(lineText)) {
|
|
27559
|
+
// Add minimal RTL compensation buffer - just enough to prevent clipping
|
|
27560
|
+
const rtlBuffer = (this.fontSize || 16) * 0.15; // 15% of font size (much smaller)
|
|
27561
|
+
maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth + rtlBuffer);
|
|
27562
|
+
} else {
|
|
27563
|
+
maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth);
|
|
27564
|
+
}
|
|
27565
|
+
}
|
|
27566
|
+
|
|
27567
|
+
// Safety margin - how close glyphs can get before we snap
|
|
27568
|
+
const safetyThreshold = 2; // px - very subtle trigger
|
|
27569
|
+
|
|
27570
|
+
if (maxRequiredWidth > this.width - safetyThreshold) {
|
|
27571
|
+
var _this$canvas5;
|
|
27572
|
+
// Set width to exactly what's needed + minimal safety margin
|
|
27573
|
+
const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
|
|
27574
|
+
|
|
27575
|
+
// Store original position before width change
|
|
27576
|
+
const originalLeft = this.left;
|
|
27577
|
+
const originalTop = this.top;
|
|
27578
|
+
const widthIncrease = newWidth - this.width;
|
|
27579
|
+
|
|
27580
|
+
// Change width
|
|
27581
|
+
this.set('width', newWidth);
|
|
27582
|
+
|
|
27583
|
+
// Force text layout recalculation
|
|
27584
|
+
this.initDimensions();
|
|
27585
|
+
|
|
27586
|
+
// Only compensate position when resizing from left handle
|
|
27587
|
+
// Right handle resize doesn't shift the text position
|
|
27588
|
+
if (resizeOrigin === 'left') {
|
|
27589
|
+
// When resizing from left, the expansion pushes text right
|
|
27590
|
+
// Compensate by moving the textbox left by the width increase
|
|
27591
|
+
this.set({
|
|
27592
|
+
'left': originalLeft - widthIncrease,
|
|
27593
|
+
'top': originalTop
|
|
27594
|
+
});
|
|
27595
|
+
}
|
|
27596
|
+
this.setCoords();
|
|
27597
|
+
|
|
27598
|
+
// Also refresh the overlay editor if it exists
|
|
27599
|
+
if (this.__overlayEditor) {
|
|
27600
|
+
setTimeout(() => {
|
|
27601
|
+
this.__overlayEditor.refresh();
|
|
27602
|
+
}, 0);
|
|
27603
|
+
}
|
|
27604
|
+
(_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
|
|
27605
|
+
}
|
|
27606
|
+
}
|
|
27607
|
+
|
|
27608
|
+
/**
|
|
27609
|
+
* Fix character selection mismatch after JSON loading for browser-wrapped fonts
|
|
27610
|
+
* @private
|
|
27611
|
+
*/
|
|
27612
|
+
_fixCharacterMappingAfterJsonLoad() {
|
|
27613
|
+
if (this._usingBrowserWrapping) {
|
|
27614
|
+
// Clear all cached states to force fresh text layout calculation
|
|
27615
|
+
this._browserWrapCache = null;
|
|
27616
|
+
this._lastDimensionState = null;
|
|
27617
|
+
|
|
27618
|
+
// Force complete re-initialization
|
|
27619
|
+
this.initDimensions();
|
|
27620
|
+
this._forceClearCache = true;
|
|
27621
|
+
|
|
27622
|
+
// Ensure canvas refresh
|
|
27623
|
+
this.setCoords();
|
|
27624
|
+
if (this.canvas) {
|
|
27625
|
+
this.canvas.requestRenderAll();
|
|
27626
|
+
}
|
|
27627
|
+
}
|
|
27628
|
+
}
|
|
27629
|
+
|
|
27630
|
+
/**
|
|
27631
|
+
* Force complete textbox re-initialization (useful after JSON loading)
|
|
27632
|
+
* Overrides Text version with Textbox-specific logic
|
|
27633
|
+
*/
|
|
27634
|
+
forceTextReinitialization() {
|
|
27635
|
+
console.log('🔄 Force reinitializing Textbox object');
|
|
27636
|
+
|
|
27637
|
+
// CRITICAL: Ensure textbox is marked as initialized
|
|
27638
|
+
this.initialized = true;
|
|
27639
|
+
|
|
27640
|
+
// Clear all caches and force dirty state
|
|
27641
|
+
this._clearCache();
|
|
27642
|
+
this.dirty = true;
|
|
27643
|
+
this.dynamicMinWidth = 0;
|
|
27644
|
+
|
|
27645
|
+
// Force isEditing false to ensure clean state
|
|
27646
|
+
this.isEditing = false;
|
|
27647
|
+
console.log(' → Set initialized=true, dirty=true, cleared caches');
|
|
27648
|
+
|
|
27649
|
+
// Re-initialize dimensions (this will handle justify properly)
|
|
27650
|
+
this.initDimensions();
|
|
27651
|
+
|
|
27652
|
+
// Double-check that justify was applied by checking space widths
|
|
27653
|
+
if (this.textAlign.includes('justify') && this.__charBounds) {
|
|
27654
|
+
setTimeout(() => {
|
|
27655
|
+
var _this$canvas6;
|
|
27656
|
+
// Verify justify was applied by checking if space widths vary
|
|
27657
|
+
let hasVariableSpaces = false;
|
|
27658
|
+
this.__charBounds.forEach((lineBounds, i) => {
|
|
27659
|
+
if (lineBounds && this._textLines && this._textLines[i]) {
|
|
27660
|
+
const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
|
|
27661
|
+
if (spaces.length > 1) {
|
|
27662
|
+
const firstSpaceWidth = spaces[0].width;
|
|
27663
|
+
hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
|
|
27664
|
+
}
|
|
27665
|
+
}
|
|
27666
|
+
});
|
|
27667
|
+
if (!hasVariableSpaces && this.__charBounds.length > 0) {
|
|
27668
|
+
console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
|
|
27669
|
+
if (this.enlargeSpaces) {
|
|
27670
|
+
this.enlargeSpaces();
|
|
27671
|
+
}
|
|
27672
|
+
} else {
|
|
27673
|
+
console.log(' ✅ Justify spaces properly expanded');
|
|
27674
|
+
}
|
|
27675
|
+
|
|
27676
|
+
// Ensure height is recalculated - use browser height if available
|
|
27677
|
+
if (this._usingBrowserWrapping && this._actualBrowserHeight) {
|
|
27678
|
+
this.height = this._actualBrowserHeight;
|
|
27679
|
+
console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
|
|
27680
|
+
} else {
|
|
27681
|
+
this.height = this.calcTextHeight();
|
|
27682
|
+
console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
|
|
27683
|
+
}
|
|
27684
|
+
(_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
|
|
27685
|
+
}, 10);
|
|
27686
|
+
}
|
|
27687
|
+
}
|
|
27688
|
+
|
|
25763
27689
|
/**
|
|
25764
27690
|
* Returns object representation of an instance
|
|
25765
27691
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|