@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.node.mjs
CHANGED
|
@@ -410,7 +410,7 @@ class Cache {
|
|
|
410
410
|
}
|
|
411
411
|
const cache = new Cache();
|
|
412
412
|
|
|
413
|
-
var version = "7.0.
|
|
413
|
+
var version = "7.0.1-beta9";
|
|
414
414
|
|
|
415
415
|
// use this syntax so babel plugin see this import here
|
|
416
416
|
const VERSION = version;
|
|
@@ -17627,33 +17627,30 @@ class PatternBrush extends PencilBrush {
|
|
|
17627
17627
|
}
|
|
17628
17628
|
}
|
|
17629
17629
|
|
|
17630
|
-
// @TODO this code is terrible and Line should be a special case of polyline.
|
|
17631
|
-
|
|
17632
17630
|
const coordProps = ['x1', 'x2', 'y1', 'y2'];
|
|
17633
|
-
/**
|
|
17634
|
-
* A Class to draw a line
|
|
17635
|
-
* A bunch of methods will be added to Polyline to handle the line case
|
|
17636
|
-
* The line class is very strange to work with, is all special, it hardly aligns
|
|
17637
|
-
* to what a developer want everytime there is an angle
|
|
17638
|
-
* @deprecated
|
|
17639
|
-
*/
|
|
17640
17631
|
class Line extends FabricObject {
|
|
17641
|
-
/**
|
|
17642
|
-
* Constructor
|
|
17643
|
-
* @param {Array} [points] Array of points
|
|
17644
|
-
* @param {Object} [options] Options object
|
|
17645
|
-
* @return {Line} thisArg
|
|
17646
|
-
*/
|
|
17647
17632
|
constructor() {
|
|
17648
|
-
let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0,
|
|
17633
|
+
let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0, 100, 0];
|
|
17649
17634
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17650
17635
|
super();
|
|
17651
|
-
|
|
17636
|
+
_defineProperty(this, "hitStrokeWidth", 'auto');
|
|
17637
|
+
_defineProperty(this, "_updatingEndpoints", false);
|
|
17638
|
+
_defineProperty(this, "_useEndpointCoords", true);
|
|
17639
|
+
_defineProperty(this, "_exportingSVG", false);
|
|
17652
17640
|
this.setOptions(options);
|
|
17653
17641
|
this.x1 = x1;
|
|
17654
17642
|
this.x2 = x2;
|
|
17655
17643
|
this.y1 = y1;
|
|
17656
17644
|
this.y2 = y2;
|
|
17645
|
+
if (options.hitStrokeWidth !== undefined) {
|
|
17646
|
+
this.hitStrokeWidth = options.hitStrokeWidth;
|
|
17647
|
+
}
|
|
17648
|
+
this.hasBorders = false;
|
|
17649
|
+
this.hasControls = true;
|
|
17650
|
+
this.selectable = true;
|
|
17651
|
+
this.hoverCursor = 'move';
|
|
17652
|
+
this.perPixelTargetFind = false;
|
|
17653
|
+
this.strokeLineCap = 'butt';
|
|
17657
17654
|
this._setWidthHeight();
|
|
17658
17655
|
const {
|
|
17659
17656
|
left,
|
|
@@ -17661,129 +17658,384 @@ class Line extends FabricObject {
|
|
|
17661
17658
|
} = options;
|
|
17662
17659
|
typeof left === 'number' && this.set(LEFT, left);
|
|
17663
17660
|
typeof top === 'number' && this.set(TOP, top);
|
|
17661
|
+
this._setupLineControls();
|
|
17662
|
+
}
|
|
17663
|
+
_setupLineControls() {
|
|
17664
|
+
this.controls = {
|
|
17665
|
+
p1: new Control({
|
|
17666
|
+
x: 0,
|
|
17667
|
+
y: 0,
|
|
17668
|
+
cursorStyle: 'move',
|
|
17669
|
+
actionHandler: this._endpointActionHandler.bind(this),
|
|
17670
|
+
positionHandler: this._p1PositionHandler.bind(this),
|
|
17671
|
+
render: this._renderEndpointControl.bind(this),
|
|
17672
|
+
sizeX: 12,
|
|
17673
|
+
sizeY: 12
|
|
17674
|
+
}),
|
|
17675
|
+
p2: new Control({
|
|
17676
|
+
x: 0,
|
|
17677
|
+
y: 0,
|
|
17678
|
+
cursorStyle: 'move',
|
|
17679
|
+
actionHandler: this._endpointActionHandler.bind(this),
|
|
17680
|
+
positionHandler: this._p2PositionHandler.bind(this),
|
|
17681
|
+
render: this._renderEndpointControl.bind(this),
|
|
17682
|
+
sizeX: 12,
|
|
17683
|
+
sizeY: 12
|
|
17684
|
+
})
|
|
17685
|
+
};
|
|
17686
|
+
}
|
|
17687
|
+
_p1PositionHandler() {
|
|
17688
|
+
return new Point(this.x1, this.y1).transform(this.getViewportTransform());
|
|
17664
17689
|
}
|
|
17690
|
+
_p2PositionHandler() {
|
|
17691
|
+
return new Point(this.x2, this.y2).transform(this.getViewportTransform());
|
|
17692
|
+
}
|
|
17693
|
+
_renderEndpointControl(ctx, left, top) {
|
|
17694
|
+
const size = 12;
|
|
17695
|
+
ctx.save();
|
|
17696
|
+
ctx.fillStyle = '#007bff';
|
|
17697
|
+
ctx.strokeStyle = '#ffffff';
|
|
17698
|
+
ctx.lineWidth = 2;
|
|
17699
|
+
ctx.beginPath();
|
|
17700
|
+
ctx.arc(left, top, size / 2, 0, 2 * Math.PI);
|
|
17701
|
+
ctx.fill();
|
|
17702
|
+
ctx.stroke();
|
|
17703
|
+
ctx.restore();
|
|
17704
|
+
}
|
|
17705
|
+
drawBorders(ctx) {
|
|
17706
|
+
let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17707
|
+
if (this._useEndpointCoords) {
|
|
17708
|
+
this._drawLineBorders(ctx, styleOverride);
|
|
17709
|
+
return this;
|
|
17710
|
+
}
|
|
17711
|
+
return super.drawBorders(ctx, styleOverride, {});
|
|
17712
|
+
}
|
|
17713
|
+
_drawLineBorders(ctx) {
|
|
17714
|
+
var _this$canvas;
|
|
17715
|
+
let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17716
|
+
const vpt = ((_this$canvas = this.canvas) === null || _this$canvas === void 0 ? void 0 : _this$canvas.viewportTransform) || [1, 0, 0, 1, 0, 0];
|
|
17717
|
+
ctx.save();
|
|
17718
|
+
ctx.setTransform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
|
|
17719
|
+
ctx.strokeStyle = styleOverride.borderColor || this.borderColor || 'rgba(100, 200, 200, 0.5)';
|
|
17720
|
+
ctx.lineWidth = (this.strokeWidth || 1) + 5;
|
|
17721
|
+
ctx.lineCap = this.strokeLineCap || 'butt';
|
|
17722
|
+
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
|
|
17723
|
+
ctx.beginPath();
|
|
17724
|
+
ctx.moveTo(this.x1, this.y1);
|
|
17725
|
+
ctx.lineTo(this.x2, this.y2);
|
|
17726
|
+
ctx.stroke();
|
|
17727
|
+
ctx.restore();
|
|
17728
|
+
}
|
|
17729
|
+
_renderControls(ctx) {
|
|
17730
|
+
let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17731
|
+
ctx.save();
|
|
17732
|
+
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
|
|
17733
|
+
this.drawControls(ctx, styleOverride);
|
|
17734
|
+
ctx.restore();
|
|
17735
|
+
}
|
|
17736
|
+
getBoundingRect() {
|
|
17737
|
+
if (this._useEndpointCoords) {
|
|
17738
|
+
const {
|
|
17739
|
+
x1,
|
|
17740
|
+
y1,
|
|
17741
|
+
x2,
|
|
17742
|
+
y2
|
|
17743
|
+
} = this;
|
|
17744
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
|
|
17745
|
+
const padding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
|
|
17746
|
+
return {
|
|
17747
|
+
left: Math.min(x1, x2) - padding,
|
|
17748
|
+
top: Math.min(y1, y2) - padding,
|
|
17749
|
+
width: Math.abs(x2 - x1) + padding * 2 || padding * 2,
|
|
17750
|
+
height: Math.abs(y2 - y1) + padding * 2 || padding * 2
|
|
17751
|
+
};
|
|
17752
|
+
}
|
|
17753
|
+
return super.getBoundingRect();
|
|
17754
|
+
}
|
|
17755
|
+
setCoords() {
|
|
17756
|
+
if (this._useEndpointCoords) {
|
|
17757
|
+
// Set width and height for hit detection and bounding box
|
|
17758
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
|
|
17759
|
+
const hitPadding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
|
|
17760
|
+
this.width = Math.abs(this.x2 - this.x1) + hitPadding * 2;
|
|
17761
|
+
this.height = Math.abs(this.y2 - this.y1) + hitPadding * 2;
|
|
17665
17762
|
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
|
|
17670
|
-
|
|
17671
|
-
|
|
17672
|
-
|
|
17673
|
-
|
|
17674
|
-
x2,
|
|
17675
|
-
y2
|
|
17676
|
-
} = this;
|
|
17677
|
-
this.width = Math.abs(x2 - x1);
|
|
17678
|
-
this.height = Math.abs(y2 - y1);
|
|
17679
|
-
const {
|
|
17680
|
-
left,
|
|
17681
|
-
top,
|
|
17682
|
-
width,
|
|
17683
|
-
height
|
|
17684
|
-
} = makeBoundingBoxFromPoints([{
|
|
17685
|
-
x: x1,
|
|
17686
|
-
y: y1
|
|
17687
|
-
}, {
|
|
17688
|
-
x: x2,
|
|
17689
|
-
y: y2
|
|
17690
|
-
}]);
|
|
17691
|
-
const position = new Point(left + width / 2, top + height / 2);
|
|
17692
|
-
this.setPositionByOrigin(position, CENTER, CENTER);
|
|
17763
|
+
// Only update left/top if they haven't been explicitly set (e.g., during loading)
|
|
17764
|
+
if (this.left === 0 && this.top === 0) {
|
|
17765
|
+
const center = this._findCenterFromElement();
|
|
17766
|
+
this.left = center.x;
|
|
17767
|
+
this.top = center.y;
|
|
17768
|
+
}
|
|
17769
|
+
}
|
|
17770
|
+
super.setCoords();
|
|
17693
17771
|
}
|
|
17772
|
+
getCoords() {
|
|
17773
|
+
if (this._useEndpointCoords) {
|
|
17774
|
+
const deltaX = this.x2 - this.x1;
|
|
17775
|
+
const deltaY = this.y2 - this.y1;
|
|
17776
|
+
const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
17777
|
+
if (length === 0) {
|
|
17778
|
+
return super.getCoords();
|
|
17779
|
+
}
|
|
17780
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
|
|
17781
|
+
const halfWidth = Math.max(effectiveStrokeWidth / 2 + 2, 5);
|
|
17694
17782
|
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17783
|
+
// Unit vector perpendicular to line
|
|
17784
|
+
const perpX = -deltaY / length;
|
|
17785
|
+
const perpY = deltaX / length;
|
|
17786
|
+
|
|
17787
|
+
// Four corners of oriented rectangle
|
|
17788
|
+
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)];
|
|
17789
|
+
}
|
|
17790
|
+
return super.getCoords();
|
|
17791
|
+
}
|
|
17792
|
+
containsPoint(point) {
|
|
17793
|
+
if (this._useEndpointCoords) {
|
|
17794
|
+
var _this$canvas2;
|
|
17795
|
+
if (((_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 ? void 0 : _this$canvas2.getActiveObject()) === this) {
|
|
17796
|
+
return super.containsPoint(point);
|
|
17797
|
+
}
|
|
17798
|
+
const distance = this._distanceToLineSegment(point.x, point.y);
|
|
17799
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth || 1;
|
|
17800
|
+
const tolerance = Math.max(effectiveStrokeWidth / 2 + 2, 5);
|
|
17801
|
+
return distance <= tolerance;
|
|
17802
|
+
}
|
|
17803
|
+
return super.containsPoint(point);
|
|
17804
|
+
}
|
|
17805
|
+
_distanceToLineSegment(px, py) {
|
|
17806
|
+
const x1 = this.x1,
|
|
17807
|
+
y1 = this.y1,
|
|
17808
|
+
x2 = this.x2,
|
|
17809
|
+
y2 = this.y2;
|
|
17810
|
+
const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
|
|
17811
|
+
if (pd2 === 0) {
|
|
17812
|
+
return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
|
|
17813
|
+
}
|
|
17814
|
+
const u = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / pd2;
|
|
17815
|
+
let closestX, closestY;
|
|
17816
|
+
if (u < 0) {
|
|
17817
|
+
closestX = x1;
|
|
17818
|
+
closestY = y1;
|
|
17819
|
+
} else if (u > 1) {
|
|
17820
|
+
closestX = x2;
|
|
17821
|
+
closestY = y2;
|
|
17822
|
+
} else {
|
|
17823
|
+
closestX = x1 + u * (x2 - x1);
|
|
17824
|
+
closestY = y1 + u * (y2 - y1);
|
|
17825
|
+
}
|
|
17826
|
+
return Math.sqrt((px - closestX) * (px - closestX) + (py - closestY) * (py - closestY));
|
|
17827
|
+
}
|
|
17828
|
+
_endpointActionHandler(eventData, transformData, x, y) {
|
|
17829
|
+
var _this$canvas4;
|
|
17830
|
+
const controlKey = transformData.corner;
|
|
17831
|
+
const pointer = new Point(x, y);
|
|
17832
|
+
let newX = pointer.x;
|
|
17833
|
+
let newY = pointer.y;
|
|
17834
|
+
if (eventData.shiftKey) {
|
|
17835
|
+
const otherControl = controlKey === 'p1' ? 'p2' : 'p1';
|
|
17836
|
+
const otherX = this[otherControl === 'p1' ? 'x1' : 'x2'];
|
|
17837
|
+
const otherY = this[otherControl === 'p1' ? 'y1' : 'y2'];
|
|
17838
|
+
const snapped = this._snapToAngle(otherX, otherY, newX, newY);
|
|
17839
|
+
newX = snapped.x;
|
|
17840
|
+
newY = snapped.y;
|
|
17841
|
+
}
|
|
17842
|
+
if (this._useEndpointCoords) {
|
|
17843
|
+
var _this$canvas3;
|
|
17844
|
+
if (controlKey === 'p1') {
|
|
17845
|
+
this.x1 = newX;
|
|
17846
|
+
this.y1 = newY;
|
|
17847
|
+
} else if (controlKey === 'p2') {
|
|
17848
|
+
this.x2 = newX;
|
|
17849
|
+
this.y2 = newY;
|
|
17850
|
+
}
|
|
17851
|
+
|
|
17852
|
+
// Update gradient coordinates if stroke is a gradient (but not during SVG export)
|
|
17853
|
+
if (this.stroke instanceof Gradient && !this._exportingSVG) {
|
|
17854
|
+
this.stroke.coords.x1 = this.x1;
|
|
17855
|
+
this.stroke.coords.y1 = this.y1;
|
|
17856
|
+
this.stroke.coords.x2 = this.x2;
|
|
17857
|
+
this.stroke.coords.y2 = this.y2;
|
|
17858
|
+
}
|
|
17859
|
+
this.dirty = true;
|
|
17860
|
+
this.setCoords();
|
|
17861
|
+
(_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
|
|
17862
|
+
return true;
|
|
17863
|
+
}
|
|
17864
|
+
|
|
17865
|
+
// Fallback for old system
|
|
17866
|
+
this._updatingEndpoints = true;
|
|
17867
|
+
if (controlKey === 'p1') {
|
|
17868
|
+
this.x1 = newX;
|
|
17869
|
+
this.y1 = newY;
|
|
17870
|
+
} else if (controlKey === 'p2') {
|
|
17871
|
+
this.x2 = newX;
|
|
17872
|
+
this.y2 = newY;
|
|
17873
|
+
}
|
|
17874
|
+
this._setWidthHeight();
|
|
17875
|
+
this.dirty = true;
|
|
17876
|
+
this._updatingEndpoints = false;
|
|
17877
|
+
(_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
|
|
17878
|
+
this.fire('modified', {
|
|
17879
|
+
transform: transformData,
|
|
17880
|
+
target: this,
|
|
17881
|
+
e: eventData
|
|
17882
|
+
});
|
|
17883
|
+
return true;
|
|
17884
|
+
}
|
|
17885
|
+
_snapToAngle(fromX, fromY, toX, toY) {
|
|
17886
|
+
const deltaX = toX - fromX;
|
|
17887
|
+
const deltaY = toY - fromY;
|
|
17888
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
17889
|
+
if (distance === 0) return {
|
|
17890
|
+
x: toX,
|
|
17891
|
+
y: toY
|
|
17892
|
+
};
|
|
17893
|
+
let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
|
|
17894
|
+
const snapIncrement = 15;
|
|
17895
|
+
const snappedAngle = Math.round(angle / snapIncrement) * snapIncrement;
|
|
17896
|
+
const snappedRadians = snappedAngle * (Math.PI / 180);
|
|
17897
|
+
return {
|
|
17898
|
+
x: fromX + Math.cos(snappedRadians) * distance,
|
|
17899
|
+
y: fromY + Math.sin(snappedRadians) * distance
|
|
17900
|
+
};
|
|
17901
|
+
}
|
|
17902
|
+
_setWidthHeight() {
|
|
17903
|
+
let skipReposition = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
|
17904
|
+
this.width = Math.abs(this.x2 - this.x1) || 1;
|
|
17905
|
+
this.height = Math.abs(this.y2 - this.y1) || 1;
|
|
17906
|
+
if (!skipReposition && !this._updatingEndpoints) {
|
|
17907
|
+
const {
|
|
17908
|
+
left,
|
|
17909
|
+
top,
|
|
17910
|
+
width,
|
|
17911
|
+
height
|
|
17912
|
+
} = makeBoundingBoxFromPoints([{
|
|
17913
|
+
x: this.x1,
|
|
17914
|
+
y: this.y1
|
|
17915
|
+
}, {
|
|
17916
|
+
x: this.x2,
|
|
17917
|
+
y: this.y2
|
|
17918
|
+
}]);
|
|
17919
|
+
this.setPositionByOrigin(new Point(left + width / 2, top + height / 2), CENTER, CENTER);
|
|
17920
|
+
}
|
|
17921
|
+
}
|
|
17700
17922
|
_set(key, value) {
|
|
17923
|
+
const oldLeft = this.left;
|
|
17924
|
+
const oldTop = this.top;
|
|
17701
17925
|
super._set(key, value);
|
|
17702
17926
|
if (coordProps.includes(key)) {
|
|
17703
|
-
// this doesn't make sense very much, since setting x1 when top or left
|
|
17704
|
-
// are already set, is just going to show a strange result since the
|
|
17705
|
-
// line will move way more than the developer expect.
|
|
17706
|
-
// in fabric5 it worked only when the line didn't have extra transformations,
|
|
17707
|
-
// in fabric6 too. With extra transform they behave bad in different ways.
|
|
17708
|
-
// This needs probably a good rework or a tutorial if you have to create a dynamic line
|
|
17709
17927
|
this._setWidthHeight();
|
|
17928
|
+
this.dirty = true;
|
|
17929
|
+
|
|
17930
|
+
// Update gradient coordinates if stroke is a gradient (but not during SVG export)
|
|
17931
|
+
if (this.stroke instanceof Gradient && !this._exportingSVG) {
|
|
17932
|
+
this.stroke.coords.x1 = this.x1;
|
|
17933
|
+
this.stroke.coords.y1 = this.y1;
|
|
17934
|
+
this.stroke.coords.x2 = this.x2;
|
|
17935
|
+
this.stroke.coords.y2 = this.y2;
|
|
17936
|
+
}
|
|
17937
|
+
}
|
|
17938
|
+
if ((key === 'left' || key === 'top') && this.canvas && !this._updatingEndpoints) {
|
|
17939
|
+
const deltaX = this.left - oldLeft;
|
|
17940
|
+
const deltaY = this.top - oldTop;
|
|
17941
|
+
if (deltaX !== 0 || deltaY !== 0) {
|
|
17942
|
+
this._updatingEndpoints = true;
|
|
17943
|
+
this.x1 += deltaX;
|
|
17944
|
+
this.y1 += deltaY;
|
|
17945
|
+
this.x2 += deltaX;
|
|
17946
|
+
this.y2 += deltaY;
|
|
17947
|
+
|
|
17948
|
+
// Update gradient coordinates if stroke is a gradient
|
|
17949
|
+
if (this.stroke instanceof Gradient) {
|
|
17950
|
+
this.stroke.coords.x1 = this.x1;
|
|
17951
|
+
this.stroke.coords.y1 = this.y1;
|
|
17952
|
+
this.stroke.coords.x2 = this.x2;
|
|
17953
|
+
this.stroke.coords.y2 = this.y2;
|
|
17954
|
+
}
|
|
17955
|
+
this._updatingEndpoints = false;
|
|
17956
|
+
}
|
|
17710
17957
|
}
|
|
17711
17958
|
return this;
|
|
17712
17959
|
}
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
|
|
17960
|
+
render(ctx) {
|
|
17961
|
+
if (this._useEndpointCoords) {
|
|
17962
|
+
this._renderDirectly(ctx);
|
|
17963
|
+
return;
|
|
17964
|
+
}
|
|
17965
|
+
super.render(ctx);
|
|
17966
|
+
}
|
|
17967
|
+
_renderDirectly(ctx) {
|
|
17968
|
+
if (!this.visible) return;
|
|
17969
|
+
ctx.save();
|
|
17970
|
+
ctx.globalAlpha = this.opacity;
|
|
17971
|
+
ctx.lineWidth = this.strokeWidth;
|
|
17972
|
+
ctx.lineCap = this.strokeLineCap || 'butt';
|
|
17973
|
+
ctx.beginPath();
|
|
17974
|
+
ctx.moveTo(this.x1, this.y1);
|
|
17975
|
+
ctx.lineTo(this.x2, this.y2);
|
|
17976
|
+
const origStrokeStyle = ctx.strokeStyle;
|
|
17977
|
+
if (isFiller(this.stroke)) {
|
|
17978
|
+
ctx.strokeStyle = this.stroke.toLive(ctx);
|
|
17979
|
+
} else {
|
|
17980
|
+
var _this$stroke;
|
|
17981
|
+
ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
|
|
17982
|
+
}
|
|
17983
|
+
ctx.stroke();
|
|
17984
|
+
ctx.strokeStyle = origStrokeStyle;
|
|
17985
|
+
ctx.restore();
|
|
17986
|
+
}
|
|
17718
17987
|
_render(ctx) {
|
|
17988
|
+
if (this._useEndpointCoords) return;
|
|
17719
17989
|
ctx.beginPath();
|
|
17720
17990
|
const p = this.calcLinePoints();
|
|
17721
17991
|
ctx.moveTo(p.x1, p.y1);
|
|
17722
17992
|
ctx.lineTo(p.x2, p.y2);
|
|
17723
17993
|
ctx.lineWidth = this.strokeWidth;
|
|
17724
|
-
|
|
17725
|
-
// TODO: test this
|
|
17726
|
-
// make sure setting "fill" changes color of a line
|
|
17727
|
-
// (by copying fillStyle to strokeStyle, since line is stroked, not filled)
|
|
17728
17994
|
const origStrokeStyle = ctx.strokeStyle;
|
|
17729
17995
|
if (isFiller(this.stroke)) {
|
|
17730
17996
|
ctx.strokeStyle = this.stroke.toLive(ctx);
|
|
17731
|
-
} else {
|
|
17732
|
-
var _this$stroke;
|
|
17733
|
-
ctx.strokeStyle = (_this$stroke = this.stroke) !== null && _this$stroke !== void 0 ? _this$stroke : ctx.fillStyle;
|
|
17734
17997
|
}
|
|
17735
17998
|
this.stroke && this._renderStroke(ctx);
|
|
17736
17999
|
ctx.strokeStyle = origStrokeStyle;
|
|
17737
18000
|
}
|
|
17738
|
-
|
|
17739
|
-
/**
|
|
17740
|
-
* This function is an helper for svg import. it returns the center of the object in the svg
|
|
17741
|
-
* untransformed coordinates
|
|
17742
|
-
* @private
|
|
17743
|
-
* @return {Point} center point from element coordinates
|
|
17744
|
-
*/
|
|
17745
18001
|
_findCenterFromElement() {
|
|
17746
18002
|
return new Point((this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
|
|
17747
18003
|
}
|
|
17748
|
-
|
|
17749
|
-
/**
|
|
17750
|
-
* Returns object representation of an instance
|
|
17751
|
-
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
17752
|
-
* @return {Object} object representation of an instance
|
|
17753
|
-
*/
|
|
17754
18004
|
toObject() {
|
|
17755
18005
|
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
18006
|
+
if (this._useEndpointCoords) {
|
|
18007
|
+
return {
|
|
18008
|
+
...super.toObject(propertiesToInclude),
|
|
18009
|
+
x1: this.x1,
|
|
18010
|
+
y1: this.y1,
|
|
18011
|
+
x2: this.x2,
|
|
18012
|
+
y2: this.y2
|
|
18013
|
+
};
|
|
18014
|
+
}
|
|
17756
18015
|
return {
|
|
17757
18016
|
...super.toObject(propertiesToInclude),
|
|
17758
18017
|
...this.calcLinePoints()
|
|
17759
18018
|
};
|
|
17760
18019
|
}
|
|
17761
|
-
|
|
17762
|
-
/*
|
|
17763
|
-
* Calculate object dimensions from its properties
|
|
17764
|
-
* @private
|
|
17765
|
-
*/
|
|
17766
18020
|
_getNonTransformedDimensions() {
|
|
17767
18021
|
const dim = super._getNonTransformedDimensions();
|
|
17768
|
-
if (this.strokeLineCap === '
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
}
|
|
17772
|
-
if (this.height === 0) {
|
|
17773
|
-
dim.x -= this.strokeWidth;
|
|
17774
|
-
}
|
|
18022
|
+
if (this.strokeLineCap === 'round') {
|
|
18023
|
+
dim.x += this.strokeWidth;
|
|
18024
|
+
dim.y += this.strokeWidth;
|
|
17775
18025
|
}
|
|
17776
18026
|
return dim;
|
|
17777
18027
|
}
|
|
17778
|
-
|
|
17779
|
-
/**
|
|
17780
|
-
* Recalculates line points given width and height
|
|
17781
|
-
* Those points are simply placed around the center,
|
|
17782
|
-
* This is not useful outside internal render functions and svg output
|
|
17783
|
-
* Is not meant to be for the developer.
|
|
17784
|
-
* @private
|
|
17785
|
-
*/
|
|
17786
18028
|
calcLinePoints() {
|
|
18029
|
+
if (this._updatingEndpoints) {
|
|
18030
|
+
const centerX = (this.x1 + this.x2) / 2;
|
|
18031
|
+
const centerY = (this.y1 + this.y2) / 2;
|
|
18032
|
+
return {
|
|
18033
|
+
x1: this.x1 - centerX,
|
|
18034
|
+
y1: this.y1 - centerY,
|
|
18035
|
+
x2: this.x2 - centerX,
|
|
18036
|
+
y2: this.y2 - centerY
|
|
18037
|
+
};
|
|
18038
|
+
}
|
|
17787
18039
|
const {
|
|
17788
18040
|
x1: _x1,
|
|
17789
18041
|
x2: _x2,
|
|
@@ -17792,48 +18044,64 @@ class Line extends FabricObject {
|
|
|
17792
18044
|
width,
|
|
17793
18045
|
height
|
|
17794
18046
|
} = this;
|
|
17795
|
-
const xMult = _x1 <= _x2 ? -1 : 1
|
|
17796
|
-
|
|
17797
|
-
x1 = xMult * width / 2,
|
|
17798
|
-
y1 = yMult * height / 2,
|
|
17799
|
-
x2 = xMult * -width / 2,
|
|
17800
|
-
y2 = yMult * -height / 2;
|
|
18047
|
+
const xMult = _x1 <= _x2 ? -1 : 1;
|
|
18048
|
+
const yMult = _y1 <= _y2 ? -1 : 1;
|
|
17801
18049
|
return {
|
|
17802
|
-
x1,
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
y2
|
|
18050
|
+
x1: xMult * width / 2,
|
|
18051
|
+
y1: yMult * height / 2,
|
|
18052
|
+
x2: xMult * -width / 2,
|
|
18053
|
+
y2: yMult * -height / 2
|
|
17806
18054
|
};
|
|
17807
18055
|
}
|
|
17808
|
-
|
|
17809
|
-
/* _FROM_SVG_START_ */
|
|
17810
|
-
|
|
17811
|
-
/**
|
|
17812
|
-
* Returns svg representation of an instance
|
|
17813
|
-
* @return {Array} an array of strings with the specific svg representation
|
|
17814
|
-
* of the instance
|
|
17815
|
-
*/
|
|
17816
18056
|
_toSVG() {
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
|
|
17823
|
-
|
|
18057
|
+
if (this._useEndpointCoords) {
|
|
18058
|
+
// Use absolute coordinates to bypass all Fabric.js transforms
|
|
18059
|
+
// Handle gradients manually for proper SVG export
|
|
18060
|
+
let strokeAttr = '';
|
|
18061
|
+
if (this.stroke instanceof Gradient) {
|
|
18062
|
+
// Let Fabric.js handle gradient definition, but we'll use the reference
|
|
18063
|
+
strokeAttr = `stroke="url(#${this.stroke.id})"`;
|
|
18064
|
+
} else {
|
|
18065
|
+
strokeAttr = `stroke="${this.stroke || 'none'}"`;
|
|
18066
|
+
}
|
|
18067
|
+
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`];
|
|
18068
|
+
} else {
|
|
18069
|
+
// Use standard calcLinePoints for legacy mode
|
|
18070
|
+
const {
|
|
18071
|
+
x1,
|
|
18072
|
+
x2,
|
|
18073
|
+
y1,
|
|
18074
|
+
y2
|
|
18075
|
+
} = this.calcLinePoints();
|
|
18076
|
+
return ['<line ', 'COMMON_PARTS', `x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`];
|
|
18077
|
+
}
|
|
17824
18078
|
}
|
|
18079
|
+
toSVG(reviver) {
|
|
18080
|
+
if (this._useEndpointCoords) {
|
|
18081
|
+
// For endpoint coords, we need to bypass transforms but still allow gradients
|
|
18082
|
+
// Let's temporarily disable transforms during SVG generation
|
|
18083
|
+
const originalLeft = this.left;
|
|
18084
|
+
const originalTop = this.top;
|
|
17825
18085
|
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
*/
|
|
18086
|
+
// Set position to center of line for gradient calculation
|
|
18087
|
+
this.left = (this.x1 + this.x2) / 2;
|
|
18088
|
+
this.top = (this.y1 + this.y2) / 2;
|
|
17830
18089
|
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
18090
|
+
// Get the SVG with standard system (for gradient handling)
|
|
18091
|
+
const standardSVG = super.toSVG(reviver);
|
|
18092
|
+
|
|
18093
|
+
// Restore original position
|
|
18094
|
+
this.left = originalLeft;
|
|
18095
|
+
this.top = originalTop;
|
|
18096
|
+
|
|
18097
|
+
// Extract gradient definition and clean up the line element
|
|
18098
|
+
// Remove the transform wrapper and update coordinates
|
|
18099
|
+
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}"`);
|
|
18100
|
+
return cleanSVG;
|
|
18101
|
+
}
|
|
18102
|
+
// Use default behavior for legacy mode
|
|
18103
|
+
return super.toSVG(reviver);
|
|
18104
|
+
}
|
|
17837
18105
|
static async fromElement(element, options, cssRules) {
|
|
17838
18106
|
const {
|
|
17839
18107
|
x1 = 0,
|
|
@@ -17844,14 +18112,6 @@ class Line extends FabricObject {
|
|
|
17844
18112
|
} = parseAttributes(element, this.ATTRIBUTE_NAMES, cssRules);
|
|
17845
18113
|
return new this([x1, y1, x2, y2], parsedAttributes);
|
|
17846
18114
|
}
|
|
17847
|
-
|
|
17848
|
-
/* _FROM_SVG_END_ */
|
|
17849
|
-
|
|
17850
|
-
/**
|
|
17851
|
-
* Returns Line instance from an object representation
|
|
17852
|
-
* @param {Object} object Object to create an instance from
|
|
17853
|
-
* @returns {Promise<Line>}
|
|
17854
|
-
*/
|
|
17855
18115
|
static fromObject(_ref) {
|
|
17856
18116
|
let {
|
|
17857
18117
|
x1,
|
|
@@ -17868,32 +18128,195 @@ class Line extends FabricObject {
|
|
|
17868
18128
|
});
|
|
17869
18129
|
}
|
|
17870
18130
|
}
|
|
18131
|
+
_defineProperty(Line, "type", 'Line');
|
|
18132
|
+
_defineProperty(Line, "cacheProperties", [...cacheProperties, ...coordProps]);
|
|
18133
|
+
_defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
|
|
18134
|
+
classRegistry.setClass(Line);
|
|
18135
|
+
classRegistry.setSVGClass(Line);
|
|
18136
|
+
|
|
17871
18137
|
/**
|
|
17872
|
-
*
|
|
17873
|
-
* @type number
|
|
18138
|
+
* Calculate the distance between two points
|
|
17874
18139
|
*/
|
|
18140
|
+
function pointDistance(p1, p2) {
|
|
18141
|
+
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
|
18142
|
+
}
|
|
18143
|
+
|
|
17875
18144
|
/**
|
|
17876
|
-
*
|
|
17877
|
-
* @type number
|
|
18145
|
+
* Normalize a vector
|
|
17878
18146
|
*/
|
|
18147
|
+
function normalizeVector(vector) {
|
|
18148
|
+
const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
|
|
18149
|
+
if (length === 0) return {
|
|
18150
|
+
x: 0,
|
|
18151
|
+
y: 0
|
|
18152
|
+
};
|
|
18153
|
+
return {
|
|
18154
|
+
x: vector.x / length,
|
|
18155
|
+
y: vector.y / length
|
|
18156
|
+
};
|
|
18157
|
+
}
|
|
18158
|
+
|
|
17879
18159
|
/**
|
|
17880
|
-
*
|
|
17881
|
-
* @type number
|
|
18160
|
+
* Get the maximum allowed radius for a corner based on adjacent edge lengths
|
|
17882
18161
|
*/
|
|
18162
|
+
function getMaxRadius(prevPoint, currentPoint, nextPoint) {
|
|
18163
|
+
const dist1 = pointDistance(prevPoint, currentPoint);
|
|
18164
|
+
const dist2 = pointDistance(currentPoint, nextPoint);
|
|
18165
|
+
return Math.min(dist1, dist2) / 2;
|
|
18166
|
+
}
|
|
18167
|
+
|
|
17883
18168
|
/**
|
|
17884
|
-
*
|
|
17885
|
-
* @type number
|
|
18169
|
+
* Calculate rounded corner data for a single corner
|
|
17886
18170
|
*/
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
|
|
17891
|
-
|
|
18171
|
+
function calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius) {
|
|
18172
|
+
// Calculate edge vectors
|
|
18173
|
+
const edge1 = {
|
|
18174
|
+
x: currentPoint.x - prevPoint.x,
|
|
18175
|
+
y: currentPoint.y - prevPoint.y
|
|
18176
|
+
};
|
|
18177
|
+
const edge2 = {
|
|
18178
|
+
x: nextPoint.x - currentPoint.x,
|
|
18179
|
+
y: nextPoint.y - currentPoint.y
|
|
18180
|
+
};
|
|
18181
|
+
|
|
18182
|
+
// Normalize edge vectors
|
|
18183
|
+
const norm1 = normalizeVector(edge1);
|
|
18184
|
+
const norm2 = normalizeVector(edge2);
|
|
18185
|
+
|
|
18186
|
+
// Calculate the maximum allowed radius
|
|
18187
|
+
const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
|
|
18188
|
+
const actualRadius = Math.min(radius, maxRadius);
|
|
18189
|
+
|
|
18190
|
+
// Calculate start and end points of the rounded corner
|
|
18191
|
+
const startPoint = {
|
|
18192
|
+
x: currentPoint.x - norm1.x * actualRadius,
|
|
18193
|
+
y: currentPoint.y - norm1.y * actualRadius
|
|
18194
|
+
};
|
|
18195
|
+
const endPoint = {
|
|
18196
|
+
x: currentPoint.x + norm2.x * actualRadius,
|
|
18197
|
+
y: currentPoint.y + norm2.y * actualRadius
|
|
18198
|
+
};
|
|
18199
|
+
|
|
18200
|
+
// Calculate control points for bezier curve
|
|
18201
|
+
// Using the magic number kRect for optimal circular approximation
|
|
18202
|
+
const controlOffset = actualRadius * kRect;
|
|
18203
|
+
const cp1 = {
|
|
18204
|
+
x: startPoint.x + norm1.x * controlOffset,
|
|
18205
|
+
y: startPoint.y + norm1.y * controlOffset
|
|
18206
|
+
};
|
|
18207
|
+
const cp2 = {
|
|
18208
|
+
x: endPoint.x - norm2.x * controlOffset,
|
|
18209
|
+
y: endPoint.y - norm2.y * controlOffset
|
|
18210
|
+
};
|
|
18211
|
+
return {
|
|
18212
|
+
corner: currentPoint,
|
|
18213
|
+
start: startPoint,
|
|
18214
|
+
end: endPoint,
|
|
18215
|
+
cp1,
|
|
18216
|
+
cp2,
|
|
18217
|
+
actualRadius
|
|
18218
|
+
};
|
|
18219
|
+
}
|
|
18220
|
+
|
|
18221
|
+
/**
|
|
18222
|
+
* Apply corner radius to a polygon defined by points
|
|
18223
|
+
*/
|
|
18224
|
+
function applyCornerRadiusToPolygon(points, radius) {
|
|
18225
|
+
let radiusAsPercentage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
18226
|
+
if (points.length < 3) {
|
|
18227
|
+
throw new Error('Polygon must have at least 3 points');
|
|
18228
|
+
}
|
|
18229
|
+
|
|
18230
|
+
// Calculate bounding box if radius is percentage-based
|
|
18231
|
+
let actualRadius = radius;
|
|
18232
|
+
if (radiusAsPercentage) {
|
|
18233
|
+
const minX = Math.min(...points.map(p => p.x));
|
|
18234
|
+
const maxX = Math.max(...points.map(p => p.x));
|
|
18235
|
+
const minY = Math.min(...points.map(p => p.y));
|
|
18236
|
+
const maxY = Math.max(...points.map(p => p.y));
|
|
18237
|
+
const width = maxX - minX;
|
|
18238
|
+
const height = maxY - minY;
|
|
18239
|
+
const minDimension = Math.min(width, height);
|
|
18240
|
+
actualRadius = radius / 100 * minDimension;
|
|
18241
|
+
}
|
|
18242
|
+
const roundedCorners = [];
|
|
18243
|
+
for (let i = 0; i < points.length; i++) {
|
|
18244
|
+
const prevIndex = (i - 1 + points.length) % points.length;
|
|
18245
|
+
const nextIndex = (i + 1) % points.length;
|
|
18246
|
+
const prevPoint = points[prevIndex];
|
|
18247
|
+
const currentPoint = points[i];
|
|
18248
|
+
const nextPoint = points[nextIndex];
|
|
18249
|
+
const roundedCorner = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, actualRadius);
|
|
18250
|
+
roundedCorners.push(roundedCorner);
|
|
18251
|
+
}
|
|
18252
|
+
return roundedCorners;
|
|
18253
|
+
}
|
|
18254
|
+
|
|
18255
|
+
/**
|
|
18256
|
+
* Render a rounded polygon to a canvas context
|
|
18257
|
+
*/
|
|
18258
|
+
function renderRoundedPolygon(ctx, roundedCorners) {
|
|
18259
|
+
let closed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
18260
|
+
if (roundedCorners.length === 0) return;
|
|
18261
|
+
ctx.beginPath();
|
|
18262
|
+
|
|
18263
|
+
// Start at the first corner's start point
|
|
18264
|
+
const firstCorner = roundedCorners[0];
|
|
18265
|
+
ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
|
|
18266
|
+
for (let i = 0; i < roundedCorners.length; i++) {
|
|
18267
|
+
const corner = roundedCorners[i];
|
|
18268
|
+
const nextIndex = (i + 1) % roundedCorners.length;
|
|
18269
|
+
const nextCorner = roundedCorners[nextIndex];
|
|
18270
|
+
|
|
18271
|
+
// Draw the rounded corner using bezier curve
|
|
18272
|
+
ctx.bezierCurveTo(corner.cp1.x, corner.cp1.y, corner.cp2.x, corner.cp2.y, corner.end.x, corner.end.y);
|
|
18273
|
+
|
|
18274
|
+
// Draw line to next corner's start point (if not the last segment in open path)
|
|
18275
|
+
if (i < roundedCorners.length - 1 || closed) {
|
|
18276
|
+
ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
|
|
18277
|
+
}
|
|
18278
|
+
}
|
|
18279
|
+
if (closed) {
|
|
18280
|
+
ctx.closePath();
|
|
18281
|
+
}
|
|
18282
|
+
}
|
|
18283
|
+
|
|
18284
|
+
/**
|
|
18285
|
+
* Generate SVG path data for a rounded polygon
|
|
18286
|
+
*/
|
|
18287
|
+
function generateRoundedPolygonPath(roundedCorners) {
|
|
18288
|
+
let closed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
|
18289
|
+
if (roundedCorners.length === 0) return '';
|
|
18290
|
+
const pathData = [];
|
|
18291
|
+
const firstCorner = roundedCorners[0];
|
|
18292
|
+
|
|
18293
|
+
// Move to first corner's start point
|
|
18294
|
+
pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
|
|
18295
|
+
for (let i = 0; i < roundedCorners.length; i++) {
|
|
18296
|
+
const corner = roundedCorners[i];
|
|
18297
|
+
const nextIndex = (i + 1) % roundedCorners.length;
|
|
18298
|
+
const nextCorner = roundedCorners[nextIndex];
|
|
18299
|
+
|
|
18300
|
+
// Add bezier curve for the rounded corner
|
|
18301
|
+
pathData.push(`C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`);
|
|
18302
|
+
|
|
18303
|
+
// Add line to next corner's start point (if not the last segment in open path)
|
|
18304
|
+
if (i < roundedCorners.length - 1 || closed) {
|
|
18305
|
+
pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
|
|
18306
|
+
}
|
|
18307
|
+
}
|
|
18308
|
+
if (closed) {
|
|
18309
|
+
pathData.push('Z');
|
|
18310
|
+
}
|
|
18311
|
+
return pathData.join(' ');
|
|
18312
|
+
}
|
|
17892
18313
|
|
|
17893
18314
|
const triangleDefaultValues = {
|
|
17894
18315
|
width: 100,
|
|
17895
|
-
height: 100
|
|
18316
|
+
height: 100,
|
|
18317
|
+
cornerRadius: 0
|
|
17896
18318
|
};
|
|
18319
|
+
const TRIANGLE_PROPS = ['cornerRadius'];
|
|
17897
18320
|
class Triangle extends FabricObject {
|
|
17898
18321
|
static getDefaults() {
|
|
17899
18322
|
return {
|
|
@@ -17912,34 +18335,90 @@ class Triangle extends FabricObject {
|
|
|
17912
18335
|
this.setOptions(options);
|
|
17913
18336
|
}
|
|
17914
18337
|
|
|
18338
|
+
/**
|
|
18339
|
+
* Get triangle points as an array of XY coordinates
|
|
18340
|
+
* @private
|
|
18341
|
+
*/
|
|
18342
|
+
_getTrianglePoints() {
|
|
18343
|
+
const widthBy2 = this.width / 2;
|
|
18344
|
+
const heightBy2 = this.height / 2;
|
|
18345
|
+
return [{
|
|
18346
|
+
x: -widthBy2,
|
|
18347
|
+
y: heightBy2
|
|
18348
|
+
},
|
|
18349
|
+
// bottom left
|
|
18350
|
+
{
|
|
18351
|
+
x: 0,
|
|
18352
|
+
y: -heightBy2
|
|
18353
|
+
},
|
|
18354
|
+
// top center
|
|
18355
|
+
{
|
|
18356
|
+
x: widthBy2,
|
|
18357
|
+
y: heightBy2
|
|
18358
|
+
} // bottom right
|
|
18359
|
+
];
|
|
18360
|
+
}
|
|
18361
|
+
|
|
17915
18362
|
/**
|
|
17916
18363
|
* @private
|
|
17917
18364
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
17918
18365
|
*/
|
|
17919
18366
|
_render(ctx) {
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
17926
|
-
|
|
18367
|
+
if (this.cornerRadius > 0) {
|
|
18368
|
+
// Render rounded triangle
|
|
18369
|
+
const points = this._getTrianglePoints();
|
|
18370
|
+
const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
|
|
18371
|
+
renderRoundedPolygon(ctx, roundedCorners, true);
|
|
18372
|
+
} else {
|
|
18373
|
+
// Render sharp triangle (original implementation)
|
|
18374
|
+
const widthBy2 = this.width / 2;
|
|
18375
|
+
const heightBy2 = this.height / 2;
|
|
18376
|
+
ctx.beginPath();
|
|
18377
|
+
ctx.moveTo(-widthBy2, heightBy2);
|
|
18378
|
+
ctx.lineTo(0, -heightBy2);
|
|
18379
|
+
ctx.lineTo(widthBy2, heightBy2);
|
|
18380
|
+
ctx.closePath();
|
|
18381
|
+
}
|
|
17927
18382
|
this._renderPaintInOrder(ctx);
|
|
17928
18383
|
}
|
|
17929
18384
|
|
|
18385
|
+
/**
|
|
18386
|
+
* Returns object representation of an instance
|
|
18387
|
+
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
18388
|
+
* @return {Object} object representation of an instance
|
|
18389
|
+
*/
|
|
18390
|
+
toObject() {
|
|
18391
|
+
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
18392
|
+
return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
|
|
18393
|
+
}
|
|
18394
|
+
|
|
17930
18395
|
/**
|
|
17931
18396
|
* Returns svg representation of an instance
|
|
17932
18397
|
* @return {Array} an array of strings with the specific svg representation
|
|
17933
18398
|
* of the instance
|
|
17934
18399
|
*/
|
|
17935
18400
|
_toSVG() {
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
points =
|
|
17939
|
-
|
|
18401
|
+
if (this.cornerRadius > 0) {
|
|
18402
|
+
// Generate rounded triangle as path
|
|
18403
|
+
const points = this._getTrianglePoints();
|
|
18404
|
+
const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
|
|
18405
|
+
const pathData = generateRoundedPolygonPath(roundedCorners, true);
|
|
18406
|
+
return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
|
|
18407
|
+
} else {
|
|
18408
|
+
// Original sharp triangle implementation
|
|
18409
|
+
const widthBy2 = this.width / 2;
|
|
18410
|
+
const heightBy2 = this.height / 2;
|
|
18411
|
+
const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
|
|
18412
|
+
return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
|
|
18413
|
+
}
|
|
17940
18414
|
}
|
|
17941
18415
|
}
|
|
18416
|
+
/**
|
|
18417
|
+
* Corner radius for rounded triangle corners
|
|
18418
|
+
* @type Number
|
|
18419
|
+
*/
|
|
17942
18420
|
_defineProperty(Triangle, "type", 'Triangle');
|
|
18421
|
+
_defineProperty(Triangle, "cacheProperties", [...cacheProperties, ...TRIANGLE_PROPS]);
|
|
17943
18422
|
_defineProperty(Triangle, "ownDefaults", triangleDefaultValues);
|
|
17944
18423
|
classRegistry.setClass(Triangle);
|
|
17945
18424
|
classRegistry.setSVGClass(Triangle);
|
|
@@ -18104,7 +18583,8 @@ const polylineDefaultValues = {
|
|
|
18104
18583
|
/**
|
|
18105
18584
|
* @deprecated transient option soon to be removed in favor of a different design
|
|
18106
18585
|
*/
|
|
18107
|
-
exactBoundingBox: false
|
|
18586
|
+
exactBoundingBox: false,
|
|
18587
|
+
cornerRadius: 0
|
|
18108
18588
|
};
|
|
18109
18589
|
class Polyline extends FabricObject {
|
|
18110
18590
|
static getDefaults() {
|
|
@@ -18318,7 +18798,7 @@ class Polyline extends FabricObject {
|
|
|
18318
18798
|
toObject() {
|
|
18319
18799
|
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
18320
18800
|
return {
|
|
18321
|
-
...super.toObject(propertiesToInclude),
|
|
18801
|
+
...super.toObject(['cornerRadius', ...propertiesToInclude]),
|
|
18322
18802
|
points: this.points.map(_ref => {
|
|
18323
18803
|
let {
|
|
18324
18804
|
x,
|
|
@@ -18338,14 +18818,28 @@ class Polyline extends FabricObject {
|
|
|
18338
18818
|
* of the instance
|
|
18339
18819
|
*/
|
|
18340
18820
|
_toSVG() {
|
|
18341
|
-
|
|
18342
|
-
|
|
18343
|
-
|
|
18344
|
-
|
|
18345
|
-
|
|
18346
|
-
|
|
18821
|
+
if (this.cornerRadius > 0 && this.points.length >= 3) {
|
|
18822
|
+
// Generate rounded polygon/polyline as path
|
|
18823
|
+
const diffX = this.pathOffset.x;
|
|
18824
|
+
const diffY = this.pathOffset.y;
|
|
18825
|
+
const adjustedPoints = this.points.map(point => ({
|
|
18826
|
+
x: point.x - diffX,
|
|
18827
|
+
y: point.y - diffY
|
|
18828
|
+
}));
|
|
18829
|
+
const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
|
|
18830
|
+
const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
|
|
18831
|
+
return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
|
|
18832
|
+
} else {
|
|
18833
|
+
// Original sharp corners implementation
|
|
18834
|
+
const points = [];
|
|
18835
|
+
const diffX = this.pathOffset.x;
|
|
18836
|
+
const diffY = this.pathOffset.y;
|
|
18837
|
+
const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
|
|
18838
|
+
for (let i = 0, len = this.points.length; i < len; i++) {
|
|
18839
|
+
points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
|
|
18840
|
+
}
|
|
18841
|
+
return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
|
|
18347
18842
|
}
|
|
18348
|
-
return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
|
|
18349
18843
|
}
|
|
18350
18844
|
|
|
18351
18845
|
/**
|
|
@@ -18361,13 +18855,24 @@ class Polyline extends FabricObject {
|
|
|
18361
18855
|
// NaN comes from parseFloat of a empty string in parser
|
|
18362
18856
|
return;
|
|
18363
18857
|
}
|
|
18364
|
-
|
|
18365
|
-
|
|
18366
|
-
|
|
18367
|
-
|
|
18368
|
-
|
|
18858
|
+
if (this.cornerRadius > 0 && len >= 3) {
|
|
18859
|
+
// Render with rounded corners
|
|
18860
|
+
const adjustedPoints = this.points.map(point => ({
|
|
18861
|
+
x: point.x - x,
|
|
18862
|
+
y: point.y - y
|
|
18863
|
+
}));
|
|
18864
|
+
const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
|
|
18865
|
+
renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
|
|
18866
|
+
} else {
|
|
18867
|
+
// Original sharp corners implementation
|
|
18868
|
+
ctx.beginPath();
|
|
18869
|
+
ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
|
|
18870
|
+
for (let i = 0; i < len; i++) {
|
|
18871
|
+
const point = this.points[i];
|
|
18872
|
+
ctx.lineTo(point.x - x, point.y - y);
|
|
18873
|
+
}
|
|
18874
|
+
!this.isOpen() && ctx.closePath();
|
|
18369
18875
|
}
|
|
18370
|
-
!this.isOpen() && ctx.closePath();
|
|
18371
18876
|
this._renderPaintInOrder(ctx);
|
|
18372
18877
|
}
|
|
18373
18878
|
|
|
@@ -18432,10 +18937,15 @@ class Polyline extends FabricObject {
|
|
|
18432
18937
|
* @type Boolean
|
|
18433
18938
|
* @default false
|
|
18434
18939
|
*/
|
|
18940
|
+
/**
|
|
18941
|
+
* Corner radius for rounded corners
|
|
18942
|
+
* @type Number
|
|
18943
|
+
* @default 0
|
|
18944
|
+
*/
|
|
18435
18945
|
_defineProperty(Polyline, "ownDefaults", polylineDefaultValues);
|
|
18436
18946
|
_defineProperty(Polyline, "type", 'Polyline');
|
|
18437
18947
|
_defineProperty(Polyline, "layoutProperties", [SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points']);
|
|
18438
|
-
_defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points']);
|
|
18948
|
+
_defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points', 'cornerRadius']);
|
|
18439
18949
|
_defineProperty(Polyline, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES]);
|
|
18440
18950
|
classRegistry.setClass(Polyline);
|
|
18441
18951
|
classRegistry.setSVGClass(Polyline);
|
|
@@ -18819,6 +19329,97 @@ function measureGraphemeWithKerning(grapheme, previousGrapheme, options, ctx) {
|
|
|
18819
19329
|
};
|
|
18820
19330
|
}
|
|
18821
19331
|
|
|
19332
|
+
/**
|
|
19333
|
+
* Get a representative character for font metrics measurement
|
|
19334
|
+
* Uses canvas to test which scripts the font actually supports
|
|
19335
|
+
*/
|
|
19336
|
+
function getRepresentativeCharacter(fontFamily) {
|
|
19337
|
+
const context = getMeasurementContext();
|
|
19338
|
+
|
|
19339
|
+
// Wait for font to be ready if possible
|
|
19340
|
+
if (typeof document !== 'undefined' && 'fonts' in document) {
|
|
19341
|
+
try {
|
|
19342
|
+
// Check if font is ready, if not, use fallback immediately
|
|
19343
|
+
if (!document.fonts.check(`16px ${fontFamily}`)) {
|
|
19344
|
+
return 'M'; // Use safe fallback while font loads
|
|
19345
|
+
}
|
|
19346
|
+
} catch (e) {
|
|
19347
|
+
// Font check failed, use fallback
|
|
19348
|
+
return 'M';
|
|
19349
|
+
}
|
|
19350
|
+
}
|
|
19351
|
+
|
|
19352
|
+
// Test characters for different scripts
|
|
19353
|
+
const testChars = [{
|
|
19354
|
+
char: 'م',
|
|
19355
|
+
script: 'Arabic'
|
|
19356
|
+
},
|
|
19357
|
+
// Arabic
|
|
19358
|
+
{
|
|
19359
|
+
char: 'א',
|
|
19360
|
+
script: 'Hebrew'
|
|
19361
|
+
},
|
|
19362
|
+
// Hebrew
|
|
19363
|
+
{
|
|
19364
|
+
char: 'अ',
|
|
19365
|
+
script: 'Devanagari'
|
|
19366
|
+
},
|
|
19367
|
+
// Hindi/Sanskrit
|
|
19368
|
+
{
|
|
19369
|
+
char: 'ا',
|
|
19370
|
+
script: 'Urdu'
|
|
19371
|
+
},
|
|
19372
|
+
// Urdu
|
|
19373
|
+
{
|
|
19374
|
+
char: 'ک',
|
|
19375
|
+
script: 'Persian'
|
|
19376
|
+
},
|
|
19377
|
+
// Persian
|
|
19378
|
+
{
|
|
19379
|
+
char: 'த',
|
|
19380
|
+
script: 'Tamil'
|
|
19381
|
+
},
|
|
19382
|
+
// Tamil
|
|
19383
|
+
{
|
|
19384
|
+
char: 'ก',
|
|
19385
|
+
script: 'Thai'
|
|
19386
|
+
},
|
|
19387
|
+
// Thai
|
|
19388
|
+
{
|
|
19389
|
+
char: 'М',
|
|
19390
|
+
script: 'Cyrillic'
|
|
19391
|
+
},
|
|
19392
|
+
// Cyrillic
|
|
19393
|
+
{
|
|
19394
|
+
char: 'Ω',
|
|
19395
|
+
script: 'Greek'
|
|
19396
|
+
},
|
|
19397
|
+
// Greek
|
|
19398
|
+
{
|
|
19399
|
+
char: 'M',
|
|
19400
|
+
script: 'Latin'
|
|
19401
|
+
} // Latin (fallback)
|
|
19402
|
+
];
|
|
19403
|
+
|
|
19404
|
+
// Set the font
|
|
19405
|
+
context.font = `16px ${fontFamily}`;
|
|
19406
|
+
|
|
19407
|
+
// Test each character to see which ones render properly
|
|
19408
|
+
// Use a more robust width check to avoid false positives
|
|
19409
|
+
const fallbackWidth = context.measureText('M').width;
|
|
19410
|
+
for (const test of testChars) {
|
|
19411
|
+
const metrics = context.measureText(test.char);
|
|
19412
|
+
|
|
19413
|
+
// Character is valid if it has width and isn't just a fallback glyph
|
|
19414
|
+
if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
|
|
19415
|
+
return test.char;
|
|
19416
|
+
}
|
|
19417
|
+
}
|
|
19418
|
+
|
|
19419
|
+
// Fallback to Latin 'M'
|
|
19420
|
+
return 'M';
|
|
19421
|
+
}
|
|
19422
|
+
|
|
18822
19423
|
/**
|
|
18823
19424
|
* Get font metrics for layout calculations
|
|
18824
19425
|
*/
|
|
@@ -18832,8 +19433,9 @@ function getFontMetrics(options) {
|
|
|
18832
19433
|
const context = getMeasurementContext();
|
|
18833
19434
|
applyFontStyle(context, options);
|
|
18834
19435
|
|
|
18835
|
-
// Use
|
|
18836
|
-
const
|
|
19436
|
+
// Use representative character based on font's primary script
|
|
19437
|
+
const sample = getRepresentativeCharacter(options.fontFamily);
|
|
19438
|
+
const metrics = context.measureText(sample);
|
|
18837
19439
|
const fontSize = options.fontSize;
|
|
18838
19440
|
|
|
18839
19441
|
// Calculate metrics with fallbacks
|
|
@@ -18885,7 +19487,11 @@ function getFontDeclaration(options) {
|
|
|
18885
19487
|
} = options;
|
|
18886
19488
|
|
|
18887
19489
|
// Normalize font family (add quotes if needed)
|
|
18888
|
-
|
|
19490
|
+
let normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
|
|
19491
|
+
|
|
19492
|
+
// Note: Font fallbacks are handled in the rendering phase only
|
|
19493
|
+
// to avoid affecting measurement calculations for text wrapping
|
|
19494
|
+
|
|
18889
19495
|
return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
|
|
18890
19496
|
}
|
|
18891
19497
|
|
|
@@ -19037,6 +19643,81 @@ const measurementCache = new MeasurementCache();
|
|
|
19037
19643
|
const kerningCache = new KerningCache();
|
|
19038
19644
|
const fontMetricsCache = new FontMetricsCache();
|
|
19039
19645
|
|
|
19646
|
+
// Set up font loading listener to clear caches when fonts change
|
|
19647
|
+
if (typeof document !== 'undefined' && 'fonts' in document) {
|
|
19648
|
+
document.fonts.addEventListener('loadingdone', () => {
|
|
19649
|
+
// Clear all caches when fonts finish loading
|
|
19650
|
+
clearAllCaches();
|
|
19651
|
+
});
|
|
19652
|
+
}
|
|
19653
|
+
|
|
19654
|
+
/**
|
|
19655
|
+
* Clear all measurement caches
|
|
19656
|
+
*/
|
|
19657
|
+
function clearAllCaches() {
|
|
19658
|
+
measurementCache.clear();
|
|
19659
|
+
kerningCache.clear();
|
|
19660
|
+
fontMetricsCache.clear();
|
|
19661
|
+
}
|
|
19662
|
+
|
|
19663
|
+
/**
|
|
19664
|
+
* Detect if a font lacks English glyph support
|
|
19665
|
+
* These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
|
|
19666
|
+
*/
|
|
19667
|
+
function fontLacksEnglishGlyphs(fontFamily) {
|
|
19668
|
+
if (typeof document === 'undefined') return false;
|
|
19669
|
+
|
|
19670
|
+
// Known fonts that lack English glyphs
|
|
19671
|
+
const knownNonEnglishFonts = ['stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani', 'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'];
|
|
19672
|
+
const lowerFontFamily = fontFamily.toLowerCase();
|
|
19673
|
+
|
|
19674
|
+
// Check known list first
|
|
19675
|
+
if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
|
|
19676
|
+
return true;
|
|
19677
|
+
}
|
|
19678
|
+
|
|
19679
|
+
// Dynamic glyph support detection
|
|
19680
|
+
const context = getMeasurementContext();
|
|
19681
|
+
context.font = `16px ${fontFamily}`;
|
|
19682
|
+
|
|
19683
|
+
// Test English characters
|
|
19684
|
+
const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
|
|
19685
|
+
const fallbackFont = 'Arial, sans-serif';
|
|
19686
|
+
|
|
19687
|
+
// Measure with target font
|
|
19688
|
+
const targetWidths = englishChars.map(char => context.measureText(char).width);
|
|
19689
|
+
|
|
19690
|
+
// Measure with fallback font
|
|
19691
|
+
context.font = `16px ${fallbackFont}`;
|
|
19692
|
+
const fallbackWidths = englishChars.map(char => context.measureText(char).width);
|
|
19693
|
+
|
|
19694
|
+
// If most measurements are identical, the font likely doesn't have English glyphs
|
|
19695
|
+
let identicalCount = 0;
|
|
19696
|
+
for (let i = 0; i < englishChars.length; i++) {
|
|
19697
|
+
if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
|
|
19698
|
+
identicalCount++;
|
|
19699
|
+
}
|
|
19700
|
+
}
|
|
19701
|
+
const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
|
|
19702
|
+
const lacksSupport = identicalCount >= lacksSupportThreshold;
|
|
19703
|
+
return lacksSupport;
|
|
19704
|
+
}
|
|
19705
|
+
|
|
19706
|
+
// Cache for font glyph detection results
|
|
19707
|
+
const fontGlyphCache = new Map();
|
|
19708
|
+
|
|
19709
|
+
/**
|
|
19710
|
+
* Cached version of font glyph detection
|
|
19711
|
+
*/
|
|
19712
|
+
function fontLacksEnglishGlyphsCached(fontFamily) {
|
|
19713
|
+
if (fontGlyphCache.has(fontFamily)) {
|
|
19714
|
+
return fontGlyphCache.get(fontFamily);
|
|
19715
|
+
}
|
|
19716
|
+
const result = fontLacksEnglishGlyphs(fontFamily);
|
|
19717
|
+
fontGlyphCache.set(fontFamily, result);
|
|
19718
|
+
return result;
|
|
19719
|
+
}
|
|
19720
|
+
|
|
19040
19721
|
/**
|
|
19041
19722
|
* Unicode and Internationalization Support
|
|
19042
19723
|
*
|
|
@@ -20220,6 +20901,15 @@ class FabricText extends StyledText {
|
|
|
20220
20901
|
* Does not return dimensions.
|
|
20221
20902
|
*/
|
|
20222
20903
|
initDimensions() {
|
|
20904
|
+
// Check if font is ready for accurate measurements
|
|
20905
|
+
// Only block initialization if it's a critical font loading situation
|
|
20906
|
+
const fontReady = this._isFontReady();
|
|
20907
|
+
if (!fontReady && !this.initialized) {
|
|
20908
|
+
// Only schedule font loading on first initialization
|
|
20909
|
+
this._scheduleInitAfterFontLoad();
|
|
20910
|
+
// Continue with fallback measurements for now
|
|
20911
|
+
}
|
|
20912
|
+
|
|
20223
20913
|
// Use advanced layout if enabled
|
|
20224
20914
|
if (this.enableAdvancedLayout && !this.path) {
|
|
20225
20915
|
return this.initDimensionsAdvanced();
|
|
@@ -20236,7 +20926,21 @@ class FabricText extends StyledText {
|
|
|
20236
20926
|
}
|
|
20237
20927
|
if (this.textAlign.includes(JUSTIFY)) {
|
|
20238
20928
|
// once text is measured we need to make space fatter to make justified text.
|
|
20239
|
-
|
|
20929
|
+
// Ensure __charBounds exists before calling enlargeSpaces
|
|
20930
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
20931
|
+
this.enlargeSpaces();
|
|
20932
|
+
} else {
|
|
20933
|
+
console.warn('⚠️ __charBounds not ready for justify alignment, deferring enlargeSpaces');
|
|
20934
|
+
// Defer the justify calculation until the next frame
|
|
20935
|
+
setTimeout(() => {
|
|
20936
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
20937
|
+
var _this$canvas;
|
|
20938
|
+
console.log('🔧 Applying deferred justify alignment');
|
|
20939
|
+
this.enlargeSpaces();
|
|
20940
|
+
(_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
|
|
20941
|
+
}
|
|
20942
|
+
}, 0);
|
|
20943
|
+
}
|
|
20240
20944
|
}
|
|
20241
20945
|
}
|
|
20242
20946
|
|
|
@@ -20245,8 +20949,9 @@ class FabricText extends StyledText {
|
|
|
20245
20949
|
*/
|
|
20246
20950
|
enlargeSpaces() {
|
|
20247
20951
|
let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
|
|
20952
|
+
const isRtl = this.direction === 'rtl';
|
|
20248
20953
|
for (let i = 0, len = this._textLines.length; i < len; i++) {
|
|
20249
|
-
if (this.textAlign
|
|
20954
|
+
if (!this.textAlign.includes('justify') && (i === len - 1 || this.isEndOfWrapping(i))) {
|
|
20250
20955
|
continue;
|
|
20251
20956
|
}
|
|
20252
20957
|
accumulatedSpace = 0;
|
|
@@ -20255,15 +20960,47 @@ class FabricText extends StyledText {
|
|
|
20255
20960
|
if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
|
|
20256
20961
|
numberOfSpaces = spaces.length;
|
|
20257
20962
|
diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
|
|
20258
|
-
|
|
20259
|
-
|
|
20260
|
-
|
|
20261
|
-
|
|
20262
|
-
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
20963
|
+
console.log(`🔧 EnlargeSpaces Line ${i}:`);
|
|
20964
|
+
console.log(` Current width: ${currentLineWidth}, Target: ${this.width}`);
|
|
20965
|
+
console.log(` Spaces: ${numberOfSpaces}, diffSpace: ${diffSpace.toFixed(2)}`);
|
|
20966
|
+
if (isRtl) {
|
|
20967
|
+
for (let j = 0; j < line.length; j++) {
|
|
20968
|
+
if (this._reSpaceAndTab.test(line[j])) ;
|
|
20969
|
+
}
|
|
20970
|
+
|
|
20971
|
+
// For RTL, we need to work backwards through the visual positions
|
|
20972
|
+
// but still update logical positions correctly
|
|
20973
|
+
let spaceCount = 0;
|
|
20974
|
+
for (let j = 0; j <= line.length; j++) {
|
|
20975
|
+
charBound = this.__charBounds[i][j];
|
|
20976
|
+
if (charBound) {
|
|
20977
|
+
if (this._reSpaceAndTab.test(line[j])) {
|
|
20978
|
+
charBound.width += diffSpace;
|
|
20979
|
+
charBound.kernedWidth += diffSpace;
|
|
20980
|
+
spaceCount++;
|
|
20981
|
+
}
|
|
20982
|
+
|
|
20983
|
+
// For RTL, shift all characters to the right by the total expansion
|
|
20984
|
+
// minus the expansion that comes after this character
|
|
20985
|
+
const remainingSpaces = numberOfSpaces - spaceCount;
|
|
20986
|
+
const shiftAmount = remainingSpaces * diffSpace;
|
|
20987
|
+
charBound.left += shiftAmount;
|
|
20988
|
+
}
|
|
20989
|
+
}
|
|
20990
|
+
} else {
|
|
20991
|
+
// LTR processing (original logic)
|
|
20992
|
+
for (let j = 0; j <= line.length; j++) {
|
|
20993
|
+
charBound = this.__charBounds[i][j];
|
|
20994
|
+
if (charBound) {
|
|
20995
|
+
if (this._reSpaceAndTab.test(line[j])) {
|
|
20996
|
+
charBound.width += diffSpace;
|
|
20997
|
+
charBound.kernedWidth += diffSpace;
|
|
20998
|
+
charBound.left += accumulatedSpace;
|
|
20999
|
+
accumulatedSpace += diffSpace;
|
|
21000
|
+
} else {
|
|
21001
|
+
charBound.left += accumulatedSpace;
|
|
21002
|
+
}
|
|
21003
|
+
}
|
|
20267
21004
|
}
|
|
20268
21005
|
}
|
|
20269
21006
|
}
|
|
@@ -20341,6 +21078,18 @@ class FabricText extends StyledText {
|
|
|
20341
21078
|
|
|
20342
21079
|
// Convert layout to legacy format for compatibility
|
|
20343
21080
|
this._convertLayoutToLegacyFormat(layout);
|
|
21081
|
+
|
|
21082
|
+
// Ensure justify alignment is properly applied for compatibility with legacy rendering
|
|
21083
|
+
if (this.textAlign.includes(JUSTIFY)) {
|
|
21084
|
+
// Force enlarge spaces after advanced layout calculation
|
|
21085
|
+
setTimeout(() => {
|
|
21086
|
+
if (this.enlargeSpaces) {
|
|
21087
|
+
var _this$canvas2;
|
|
21088
|
+
this.enlargeSpaces();
|
|
21089
|
+
(_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.renderAll();
|
|
21090
|
+
}
|
|
21091
|
+
}, 0);
|
|
21092
|
+
}
|
|
20344
21093
|
this.dirty = true;
|
|
20345
21094
|
}
|
|
20346
21095
|
|
|
@@ -20921,7 +21670,15 @@ class FabricText extends StyledText {
|
|
|
20921
21670
|
if (currentDirection !== this.direction) {
|
|
20922
21671
|
ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
|
|
20923
21672
|
ctx.direction = isLtr ? 'ltr' : 'rtl';
|
|
20924
|
-
|
|
21673
|
+
|
|
21674
|
+
// For justify alignments, we need to set the correct canvas text alignment
|
|
21675
|
+
// This is crucial for RTL text to render in the correct order
|
|
21676
|
+
if (isJustify) {
|
|
21677
|
+
// Justify uses LEFT alignment as a base, letting the character positioning handle justification
|
|
21678
|
+
ctx.textAlign = LEFT;
|
|
21679
|
+
} else {
|
|
21680
|
+
ctx.textAlign = isLtr ? LEFT : RIGHT;
|
|
21681
|
+
}
|
|
20925
21682
|
}
|
|
20926
21683
|
top -= lineHeight * this._fontSizeFraction / this.lineHeight;
|
|
20927
21684
|
if (shortCut) {
|
|
@@ -21157,9 +21914,21 @@ class FabricText extends StyledText {
|
|
|
21157
21914
|
direction = this.direction,
|
|
21158
21915
|
isEndOfWrapping = this.isEndOfWrapping(lineIndex);
|
|
21159
21916
|
let leftOffset = 0;
|
|
21160
|
-
|
|
21161
|
-
|
|
21917
|
+
|
|
21918
|
+
// Handle justify alignments (excluding last lines and wrapped line ends)
|
|
21919
|
+
const isJustifyLine = textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping;
|
|
21920
|
+
if (isJustifyLine) {
|
|
21921
|
+
// Justify lines should start at the left edge for LTR and right edge for RTL
|
|
21922
|
+
// The space distribution is handled by enlargeSpaces()
|
|
21923
|
+
if (direction === 'rtl') {
|
|
21924
|
+
// For RTL justify, we need to account for the line being right-aligned
|
|
21925
|
+
return 0;
|
|
21926
|
+
} else {
|
|
21927
|
+
return 0;
|
|
21928
|
+
}
|
|
21162
21929
|
}
|
|
21930
|
+
|
|
21931
|
+
// Handle non-justify alignments
|
|
21163
21932
|
if (textAlign === CENTER) {
|
|
21164
21933
|
leftOffset = lineDiff / 2;
|
|
21165
21934
|
}
|
|
@@ -21172,6 +21941,8 @@ class FabricText extends StyledText {
|
|
|
21172
21941
|
if (textAlign === JUSTIFY_RIGHT) {
|
|
21173
21942
|
leftOffset = lineDiff;
|
|
21174
21943
|
}
|
|
21944
|
+
|
|
21945
|
+
// Apply RTL adjustments for non-justify alignments
|
|
21175
21946
|
if (direction === 'rtl') {
|
|
21176
21947
|
if (textAlign === RIGHT || textAlign === JUSTIFY || textAlign === JUSTIFY_RIGHT) {
|
|
21177
21948
|
leftOffset = 0;
|
|
@@ -21330,7 +22101,19 @@ class FabricText extends StyledText {
|
|
|
21330
22101
|
fontSize = this.fontSize
|
|
21331
22102
|
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
21332
22103
|
let forMeasuring = arguments.length > 1 ? arguments[1] : undefined;
|
|
21333
|
-
|
|
22104
|
+
let parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
|
|
22105
|
+
|
|
22106
|
+
// For fonts like STV that don't support English/Latin characters,
|
|
22107
|
+
// add fallback fonts for consistent rendering of unsupported characters
|
|
22108
|
+
// Only add fallbacks during actual rendering, not for measurements
|
|
22109
|
+
if (!forMeasuring &&
|
|
22110
|
+
// Only during rendering, not measuring
|
|
22111
|
+
!fontFamily.includes(',') && (
|
|
22112
|
+
// Don't add fallbacks if already has them
|
|
22113
|
+
fontFamily.toLowerCase().includes('stv') || fontFamily.toLowerCase().includes('arabic') || fontFamily.toLowerCase().includes('naskh') || fontFamily.toLowerCase().includes('kufi'))) {
|
|
22114
|
+
// Add fallback fonts for unsupported characters (spaces, punctuation, etc.)
|
|
22115
|
+
parsedFontFamily = `${parsedFontFamily}, "Arial Unicode MS", Arial, sans-serif`;
|
|
22116
|
+
}
|
|
21334
22117
|
return [fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily].join(' ');
|
|
21335
22118
|
}
|
|
21336
22119
|
|
|
@@ -21374,7 +22157,13 @@ class FabricText extends StyledText {
|
|
|
21374
22157
|
newLine = ['\n'];
|
|
21375
22158
|
let newText = [];
|
|
21376
22159
|
for (let i = 0; i < lines.length; i++) {
|
|
21377
|
-
|
|
22160
|
+
// Use BiDi-aware grapheme splitting for RTL text
|
|
22161
|
+
if (this.direction === 'rtl' || this._containsArabicText(lines[i])) {
|
|
22162
|
+
newLines[i] = segmentGraphemes(lines[i]);
|
|
22163
|
+
console.log(`🔤 BiDi-aware split line ${i}: "${lines[i]}" -> [${newLines[i].join(', ')}]`);
|
|
22164
|
+
} else {
|
|
22165
|
+
newLines[i] = this.graphemeSplit(lines[i]);
|
|
22166
|
+
}
|
|
21378
22167
|
newText = newText.concat(newLines[i], newLine);
|
|
21379
22168
|
}
|
|
21380
22169
|
newText.pop();
|
|
@@ -21386,6 +22175,14 @@ class FabricText extends StyledText {
|
|
|
21386
22175
|
};
|
|
21387
22176
|
}
|
|
21388
22177
|
|
|
22178
|
+
/**
|
|
22179
|
+
* Check if text contains Arabic characters
|
|
22180
|
+
* @private
|
|
22181
|
+
*/
|
|
22182
|
+
_containsArabicText(text) {
|
|
22183
|
+
return /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
22184
|
+
}
|
|
22185
|
+
|
|
21389
22186
|
/**
|
|
21390
22187
|
* Returns object representation of an instance
|
|
21391
22188
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
@@ -21508,6 +22305,88 @@ class FabricText extends StyledText {
|
|
|
21508
22305
|
|
|
21509
22306
|
/* _FROM_SVG_END_ */
|
|
21510
22307
|
|
|
22308
|
+
/**
|
|
22309
|
+
* Check if the font is ready for accurate measurements
|
|
22310
|
+
* @private
|
|
22311
|
+
*/
|
|
22312
|
+
_isFontReady() {
|
|
22313
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
22314
|
+
return true; // Assume ready in non-browser environments
|
|
22315
|
+
}
|
|
22316
|
+
try {
|
|
22317
|
+
return document.fonts.check(`${this.fontSize}px ${this.fontFamily}`);
|
|
22318
|
+
} catch (e) {
|
|
22319
|
+
return true; // Fallback to assuming ready if check fails
|
|
22320
|
+
}
|
|
22321
|
+
}
|
|
22322
|
+
|
|
22323
|
+
/**
|
|
22324
|
+
* Schedule re-initialization after font loads
|
|
22325
|
+
* @private
|
|
22326
|
+
*/
|
|
22327
|
+
_scheduleInitAfterFontLoad() {
|
|
22328
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
22329
|
+
return;
|
|
22330
|
+
}
|
|
22331
|
+
|
|
22332
|
+
// Only schedule if not already waiting
|
|
22333
|
+
if (this._fontLoadScheduled) {
|
|
22334
|
+
return;
|
|
22335
|
+
}
|
|
22336
|
+
this._fontLoadScheduled = true;
|
|
22337
|
+
const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
|
|
22338
|
+
document.fonts.load(fontSpec).then(() => {
|
|
22339
|
+
this._fontLoadScheduled = false;
|
|
22340
|
+
// Re-initialize dimensions with proper font metrics
|
|
22341
|
+
this.initDimensions();
|
|
22342
|
+
|
|
22343
|
+
// Extra step for justify alignment after font loading
|
|
22344
|
+
if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
|
|
22345
|
+
setTimeout(() => {
|
|
22346
|
+
var _this$canvas3;
|
|
22347
|
+
if (this.enlargeSpaces) {
|
|
22348
|
+
this.enlargeSpaces();
|
|
22349
|
+
}
|
|
22350
|
+
(_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
|
|
22351
|
+
}, 10);
|
|
22352
|
+
} else {
|
|
22353
|
+
var _this$canvas4;
|
|
22354
|
+
(_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
|
|
22355
|
+
}
|
|
22356
|
+
}).catch(() => {
|
|
22357
|
+
this._fontLoadScheduled = false;
|
|
22358
|
+
});
|
|
22359
|
+
}
|
|
22360
|
+
|
|
22361
|
+
/**
|
|
22362
|
+
* Force complete text re-initialization (useful after JSON loading)
|
|
22363
|
+
*/
|
|
22364
|
+
forceTextReinitialization() {
|
|
22365
|
+
console.log('🔄 Force reinitializing text object');
|
|
22366
|
+
|
|
22367
|
+
// Clear all caches
|
|
22368
|
+
this._clearCache();
|
|
22369
|
+
this.dirty = true;
|
|
22370
|
+
|
|
22371
|
+
// Force text splitting to rebuild internal structures
|
|
22372
|
+
this._splitText();
|
|
22373
|
+
|
|
22374
|
+
// Re-initialize dimensions
|
|
22375
|
+
this.initDimensions();
|
|
22376
|
+
|
|
22377
|
+
// Special handling for justify alignment
|
|
22378
|
+
if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
|
|
22379
|
+
// Ensure justify is applied after dimensions are set
|
|
22380
|
+
setTimeout(() => {
|
|
22381
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
22382
|
+
var _this$canvas5;
|
|
22383
|
+
this.enlargeSpaces();
|
|
22384
|
+
(_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
|
|
22385
|
+
}
|
|
22386
|
+
}, 10);
|
|
22387
|
+
}
|
|
22388
|
+
}
|
|
22389
|
+
|
|
21511
22390
|
/**
|
|
21512
22391
|
* Returns FabricText instance from an object representation
|
|
21513
22392
|
* @param {Object} object plain js Object to create an instance from
|
|
@@ -21519,6 +22398,93 @@ class FabricText extends StyledText {
|
|
|
21519
22398
|
styles: stylesFromArray(object.styles || {}, object.text)
|
|
21520
22399
|
}, {
|
|
21521
22400
|
extraParam: 'text'
|
|
22401
|
+
}).then(textObject => {
|
|
22402
|
+
// Ensure text object is properly initialized after JSON deserialization
|
|
22403
|
+
// This is critical for justify alignment and other text layout features
|
|
22404
|
+
textObject.initialized = true;
|
|
22405
|
+
|
|
22406
|
+
// Force reinitialization to ensure proper layout
|
|
22407
|
+
if (textObject._clearCache) {
|
|
22408
|
+
textObject._clearCache();
|
|
22409
|
+
}
|
|
22410
|
+
textObject.dirty = true;
|
|
22411
|
+
|
|
22412
|
+
// Check if we need to wait for font loading (especially for custom fonts like STV)
|
|
22413
|
+
const fontSpec = `${textObject.fontSize}px ${textObject.fontFamily}`;
|
|
22414
|
+
|
|
22415
|
+
// For custom fonts, ensure they're loaded before initializing dimensions
|
|
22416
|
+
if (typeof document !== 'undefined' && 'fonts' in document && textObject.fontFamily !== 'Arial' && textObject.fontFamily !== 'Times New Roman') {
|
|
22417
|
+
return document.fonts.load(fontSpec).then(() => {
|
|
22418
|
+
var _textObject$fontFamil;
|
|
22419
|
+
console.log(`🔤 Font loaded for JSON object: ${fontSpec}`);
|
|
22420
|
+
// Ensure initialized flag is set again (in case constructor reset it)
|
|
22421
|
+
textObject.initialized = true;
|
|
22422
|
+
|
|
22423
|
+
// Special handling for STV fonts which have measurement issues
|
|
22424
|
+
const isStvFont = (_textObject$fontFamil = textObject.fontFamily) === null || _textObject$fontFamil === void 0 ? void 0 : _textObject$fontFamil.toLowerCase().includes('stv');
|
|
22425
|
+
if (isStvFont) {
|
|
22426
|
+
console.log(`🔤 STV font detected, using enhanced reinitialization`);
|
|
22427
|
+
|
|
22428
|
+
// Clear all cached state that might interfere with browser wrapping
|
|
22429
|
+
textObject._browserWrapCache = null;
|
|
22430
|
+
textObject._lastDimensionState = null;
|
|
22431
|
+
textObject._browserWrapInitialized = false;
|
|
22432
|
+
console.log(`🔤 STV font: Cleared all cached states for fresh initialization`);
|
|
22433
|
+
|
|
22434
|
+
// Force browser wrapping flag for STV fonts
|
|
22435
|
+
textObject._usingBrowserWrapping = true;
|
|
22436
|
+
console.log(`🔤 STV font: Forcing browser wrapping flag during JSON load`);
|
|
22437
|
+
|
|
22438
|
+
// Multiple initialization attempts for STV fonts
|
|
22439
|
+
const reinitWithDelay = attempt => {
|
|
22440
|
+
if (textObject.forceTextReinitialization) {
|
|
22441
|
+
textObject.forceTextReinitialization();
|
|
22442
|
+
} else {
|
|
22443
|
+
textObject.initDimensions();
|
|
22444
|
+
}
|
|
22445
|
+
|
|
22446
|
+
// Check if width is still problematic after initialization
|
|
22447
|
+
if (textObject.width < 50 && attempt < 3) {
|
|
22448
|
+
console.log(`🔤 STV font width still ${textObject.width}px, retrying in ${100 * attempt}ms (attempt ${attempt + 1}/3)`);
|
|
22449
|
+
setTimeout(() => reinitWithDelay(attempt + 1), 100 * attempt);
|
|
22450
|
+
}
|
|
22451
|
+
};
|
|
22452
|
+
reinitWithDelay(0);
|
|
22453
|
+
} else {
|
|
22454
|
+
// Use specialized reinitialization for Textbox objects
|
|
22455
|
+
if (textObject.forceTextReinitialization) {
|
|
22456
|
+
console.log(`🔤 Using Textbox specialized reinitialization`);
|
|
22457
|
+
textObject.forceTextReinitialization();
|
|
22458
|
+
} else {
|
|
22459
|
+
// Reinitialize dimensions with proper font metrics
|
|
22460
|
+
textObject.initDimensions();
|
|
22461
|
+
}
|
|
22462
|
+
}
|
|
22463
|
+
return textObject;
|
|
22464
|
+
}).catch(() => {
|
|
22465
|
+
console.warn(`⚠️ Font loading failed for ${fontSpec}, proceeding with fallback`);
|
|
22466
|
+
// Ensure initialized flag is set again
|
|
22467
|
+
textObject.initialized = true;
|
|
22468
|
+
|
|
22469
|
+
// Still initialize dimensions even if font loading fails
|
|
22470
|
+
if (textObject.forceTextReinitialization) {
|
|
22471
|
+
textObject.forceTextReinitialization();
|
|
22472
|
+
} else {
|
|
22473
|
+
textObject.initDimensions();
|
|
22474
|
+
}
|
|
22475
|
+
return textObject;
|
|
22476
|
+
});
|
|
22477
|
+
} else {
|
|
22478
|
+
// Standard fonts - ensure initialized and use appropriate method
|
|
22479
|
+
textObject.initialized = true;
|
|
22480
|
+
if (textObject.forceTextReinitialization) {
|
|
22481
|
+
console.log(`🔤 Using Textbox specialized reinitialization for standard font`);
|
|
22482
|
+
textObject.forceTextReinitialization();
|
|
22483
|
+
} else {
|
|
22484
|
+
textObject.initDimensions();
|
|
22485
|
+
}
|
|
22486
|
+
return textObject;
|
|
22487
|
+
}
|
|
21522
22488
|
});
|
|
21523
22489
|
}
|
|
21524
22490
|
}
|
|
@@ -21958,8 +22924,12 @@ class OverlayEditor {
|
|
|
21958
22924
|
this.textarea.style.pointerEvents = 'auto';
|
|
21959
22925
|
// Set appropriate unicodeBidi based on content and direction
|
|
21960
22926
|
const hasArabicText = /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(this.target.text || '');
|
|
22927
|
+
const hasLatinText = /[a-zA-Z]/.test(this.target.text || '');
|
|
21961
22928
|
const isLTRDirection = this.target.direction === 'ltr';
|
|
21962
|
-
if (hasArabicText && isLTRDirection) {
|
|
22929
|
+
if (hasArabicText && hasLatinText && isLTRDirection) {
|
|
22930
|
+
// For mixed Arabic/Latin text in LTR mode, use embed for consistent line wrapping
|
|
22931
|
+
this.textarea.style.unicodeBidi = 'embed';
|
|
22932
|
+
} else if (hasArabicText && isLTRDirection) {
|
|
21963
22933
|
// For Arabic text in LTR mode, use embed to preserve shaping while respecting direction
|
|
21964
22934
|
this.textarea.style.unicodeBidi = 'embed';
|
|
21965
22935
|
} else {
|
|
@@ -22048,14 +23018,26 @@ class OverlayEditor {
|
|
|
22048
23018
|
parseFloat(this.hostDiv.style.width) / zoom;
|
|
22049
23019
|
const currentHeight = parseFloat(this.hostDiv.style.height) / zoom;
|
|
22050
23020
|
|
|
22051
|
-
//
|
|
23021
|
+
// Always update height for responsive controls (especially important for line deletion)
|
|
22052
23022
|
const heightDiff = Math.abs(currentHeight - target.height);
|
|
22053
|
-
const threshold =
|
|
23023
|
+
const threshold = 0.5; // Lower threshold for better responsiveness to line changes
|
|
22054
23024
|
|
|
22055
23025
|
if (heightDiff > threshold) {
|
|
23026
|
+
target.height;
|
|
22056
23027
|
target.height = currentHeight;
|
|
22057
23028
|
target.setCoords(); // Update control positions
|
|
23029
|
+
|
|
23030
|
+
// Force dirty to ensure proper re-rendering
|
|
23031
|
+
target.dirty = true;
|
|
22058
23032
|
this.canvas.requestRenderAll(); // Re-render to show updated selection
|
|
23033
|
+
|
|
23034
|
+
// IMPORTANT: Reposition overlay after height change
|
|
23035
|
+
requestAnimationFrame(() => {
|
|
23036
|
+
if (!this.isDestroyed) {
|
|
23037
|
+
this.applyOverlayStyle();
|
|
23038
|
+
console.log('📐 Height changed - rechecking alignment after repositioning:');
|
|
23039
|
+
}
|
|
23040
|
+
});
|
|
22059
23041
|
}
|
|
22060
23042
|
}
|
|
22061
23043
|
|
|
@@ -22083,14 +23065,6 @@ class OverlayEditor {
|
|
|
22083
23065
|
target.setCoords();
|
|
22084
23066
|
const aCoords = target.aCoords;
|
|
22085
23067
|
|
|
22086
|
-
// DEBUG: Log dimensions before edit
|
|
22087
|
-
console.log('BEFORE EDIT:');
|
|
22088
|
-
console.log(' target.width =', target.width);
|
|
22089
|
-
console.log(' target.height =', target.height);
|
|
22090
|
-
console.log(' target.getScaledWidth() =', target.getScaledWidth());
|
|
22091
|
-
console.log(' target.getScaledHeight() =', target.getScaledHeight());
|
|
22092
|
-
console.log(' target.padding =', target.padding);
|
|
22093
|
-
|
|
22094
23068
|
// 2. Get canvas position and scroll offsets (like rtl-test.html)
|
|
22095
23069
|
const canvasEl = canvas.upperCanvasEl;
|
|
22096
23070
|
const canvasRect = canvasEl.getBoundingClientRect();
|
|
@@ -22113,14 +23087,12 @@ class OverlayEditor {
|
|
|
22113
23087
|
const left = canvasRect.left + scrollX + screenPoint.x;
|
|
22114
23088
|
const top = canvasRect.top + scrollY + screenPoint.y;
|
|
22115
23089
|
|
|
22116
|
-
// 4.
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
console.log(' zoom =', zoom);
|
|
22123
|
-
console.log(' final width =', width);
|
|
23090
|
+
// 4. Calculate the precise width and height for the container
|
|
23091
|
+
// **THE FIX:** Use getBoundingRect() for BOTH width and height.
|
|
23092
|
+
// This is the most reliable measure of the object's final rendered dimensions.
|
|
23093
|
+
const objectBounds = target.getBoundingRect();
|
|
23094
|
+
const width = Math.round(objectBounds.width * zoom);
|
|
23095
|
+
const height = Math.round(objectBounds.height * zoom);
|
|
22124
23096
|
|
|
22125
23097
|
// 5. Apply styles to host DIV - absolute positioning like rtl-test.html
|
|
22126
23098
|
this.hostDiv.style.position = 'absolute';
|
|
@@ -22144,50 +23116,333 @@ class OverlayEditor {
|
|
|
22144
23116
|
const scaleX = target.scaleX || 1;
|
|
22145
23117
|
const finalFontSize = baseFontSize * scaleX * zoom;
|
|
22146
23118
|
const fabricLineHeight = target.lineHeight || 1.16;
|
|
22147
|
-
//
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
23119
|
+
// **THE FIX:** Use 'border-box' so the width property includes padding.
|
|
23120
|
+
// This makes alignment much easier and more reliable.
|
|
23121
|
+
this.textarea.style.boxSizing = 'border-box';
|
|
23122
|
+
|
|
23123
|
+
// **THE FIX:** Set the textarea width to be IDENTICAL to the host div's width.
|
|
23124
|
+
// The padding will now be correctly contained *inside* this width.
|
|
23125
|
+
this.textarea.style.width = `${width}px`;
|
|
23126
|
+
this.textarea.style.height = '100%'; // Let hostDiv control height
|
|
22152
23127
|
this.textarea.style.padding = `${paddingY}px ${paddingX}px`;
|
|
23128
|
+
|
|
23129
|
+
// Apply all other font and text styles to match Fabric
|
|
23130
|
+
const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
|
|
23131
|
+
|
|
23132
|
+
// Special handling for text objects loaded from JSON - ensure they're properly initialized
|
|
23133
|
+
if (target.dirty !== false && target.initDimensions) {
|
|
23134
|
+
console.log('🔧 Ensuring text object is properly initialized before overlay editing');
|
|
23135
|
+
// Force re-initialization if the text object seems to be in a dirty state
|
|
23136
|
+
target.initDimensions();
|
|
23137
|
+
}
|
|
22153
23138
|
this.textarea.style.fontSize = `${finalFontSize}px`;
|
|
22154
|
-
this.textarea.style.lineHeight = String(fabricLineHeight);
|
|
23139
|
+
this.textarea.style.lineHeight = String(fabricLineHeight);
|
|
22155
23140
|
this.textarea.style.fontFamily = target.fontFamily || 'Arial';
|
|
22156
23141
|
this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
|
|
22157
23142
|
this.textarea.style.fontStyle = target.fontStyle || 'normal';
|
|
22158
|
-
|
|
23143
|
+
// Handle text alignment and justification
|
|
23144
|
+
const textAlign = target.textAlign || 'left';
|
|
23145
|
+
let cssTextAlign = textAlign;
|
|
23146
|
+
|
|
23147
|
+
// Detect text direction from content for proper justify handling
|
|
23148
|
+
const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
|
|
23149
|
+
|
|
23150
|
+
// DEBUG: Log alignment details
|
|
23151
|
+
console.log('🔍 ALIGNMENT DEBUG:');
|
|
23152
|
+
console.log(' Fabric textAlign:', textAlign);
|
|
23153
|
+
console.log(' Fabric direction:', target.direction);
|
|
23154
|
+
console.log(' Text content:', JSON.stringify(target.text));
|
|
23155
|
+
console.log(' Detected direction:', autoDetectedDirection);
|
|
23156
|
+
|
|
23157
|
+
// Map fabric.js justify to CSS
|
|
23158
|
+
if (textAlign.includes('justify')) {
|
|
23159
|
+
// Try to match fabric.js justify behavior more precisely
|
|
23160
|
+
try {
|
|
23161
|
+
// For justify, we need to replicate fabric.js space expansion
|
|
23162
|
+
// Use CSS justify but with specific settings to match fabric.js better
|
|
23163
|
+
cssTextAlign = 'justify';
|
|
23164
|
+
|
|
23165
|
+
// Set text-align-last based on justify type and detected direction
|
|
23166
|
+
// Smart justify: respect detected direction even when fabric alignment doesn't match
|
|
23167
|
+
if (textAlign === 'justify') {
|
|
23168
|
+
this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
|
|
23169
|
+
} else if (textAlign === 'justify-left') {
|
|
23170
|
+
// If text is RTL but fabric says justify-left, override to justify-right for better UX
|
|
23171
|
+
if (autoDetectedDirection === 'rtl') {
|
|
23172
|
+
this.textarea.style.textAlignLast = 'right';
|
|
23173
|
+
console.log(' → Overrode justify-left to justify-right for RTL text');
|
|
23174
|
+
} else {
|
|
23175
|
+
this.textarea.style.textAlignLast = 'left';
|
|
23176
|
+
}
|
|
23177
|
+
} else if (textAlign === 'justify-right') {
|
|
23178
|
+
// If text is LTR but fabric says justify-right, override to justify-left for better UX
|
|
23179
|
+
if (autoDetectedDirection === 'ltr') {
|
|
23180
|
+
this.textarea.style.textAlignLast = 'left';
|
|
23181
|
+
console.log(' → Overrode justify-right to justify-left for LTR text');
|
|
23182
|
+
} else {
|
|
23183
|
+
this.textarea.style.textAlignLast = 'right';
|
|
23184
|
+
}
|
|
23185
|
+
} else if (textAlign === 'justify-center') {
|
|
23186
|
+
this.textarea.style.textAlignLast = 'center';
|
|
23187
|
+
}
|
|
23188
|
+
|
|
23189
|
+
// Enhanced justify settings for better fabric.js matching
|
|
23190
|
+
this.textarea.style.textJustify = 'inter-word';
|
|
23191
|
+
this.textarea.style.wordSpacing = 'normal';
|
|
23192
|
+
|
|
23193
|
+
// Additional CSS properties for better justify matching
|
|
23194
|
+
this.textarea.style.textAlign = 'justify';
|
|
23195
|
+
this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
|
|
23196
|
+
|
|
23197
|
+
// Try to force better justify behavior
|
|
23198
|
+
this.textarea.style.textJustifyTrim = 'none';
|
|
23199
|
+
this.textarea.style.textAutospace = 'none';
|
|
23200
|
+
console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
|
|
23201
|
+
} catch (error) {
|
|
23202
|
+
console.warn(' → Justify setup failed, falling back to standard alignment:', error);
|
|
23203
|
+
cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
|
|
23204
|
+
}
|
|
23205
|
+
} else {
|
|
23206
|
+
this.textarea.style.textAlignLast = 'auto';
|
|
23207
|
+
this.textarea.style.textJustify = 'auto';
|
|
23208
|
+
this.textarea.style.wordSpacing = 'normal';
|
|
23209
|
+
console.log(' → Applied standard alignment:', cssTextAlign);
|
|
23210
|
+
}
|
|
23211
|
+
this.textarea.style.textAlign = cssTextAlign;
|
|
22159
23212
|
this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
|
|
22160
|
-
this.textarea.style.letterSpacing = `${
|
|
22161
|
-
this.textarea.style.direction = target.direction || this.firstStrongDir(this.textarea.value || '');
|
|
23213
|
+
this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
|
|
22162
23214
|
|
|
22163
|
-
//
|
|
23215
|
+
// Use the already detected direction from above
|
|
23216
|
+
const fabricDirection = target.direction;
|
|
23217
|
+
|
|
23218
|
+
// Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
|
|
23219
|
+
this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
|
|
22164
23220
|
this.textarea.style.fontVariant = 'normal';
|
|
22165
23221
|
this.textarea.style.fontStretch = 'normal';
|
|
22166
|
-
this.textarea.style.textRendering = 'auto';
|
|
22167
|
-
this.textarea.style.fontKerning = '
|
|
22168
|
-
this.textarea.style.
|
|
23222
|
+
this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
|
|
23223
|
+
this.textarea.style.fontKerning = 'normal';
|
|
23224
|
+
this.textarea.style.fontFeatureSettings = 'normal';
|
|
23225
|
+
this.textarea.style.fontVariationSettings = 'normal';
|
|
22169
23226
|
this.textarea.style.margin = '0';
|
|
22170
23227
|
this.textarea.style.border = 'none';
|
|
22171
23228
|
this.textarea.style.outline = 'none';
|
|
22172
23229
|
this.textarea.style.background = 'transparent';
|
|
22173
|
-
this.textarea.style.
|
|
23230
|
+
this.textarea.style.overflowWrap = 'break-word';
|
|
22174
23231
|
this.textarea.style.whiteSpace = 'pre-wrap';
|
|
22175
|
-
|
|
22176
|
-
|
|
22177
|
-
|
|
22178
|
-
console.log('
|
|
22179
|
-
console.log('
|
|
22180
|
-
console.log('
|
|
22181
|
-
console.log('
|
|
22182
|
-
console.log('
|
|
22183
|
-
console.log('
|
|
22184
|
-
console.log('
|
|
22185
|
-
console.log('
|
|
22186
|
-
console.log('
|
|
23232
|
+
this.textarea.style.hyphens = 'none';
|
|
23233
|
+
|
|
23234
|
+
// DEBUG: Log final CSS properties
|
|
23235
|
+
console.log('🎨 FINAL TEXTAREA CSS:');
|
|
23236
|
+
console.log(' textAlign:', this.textarea.style.textAlign);
|
|
23237
|
+
console.log(' textAlignLast:', this.textarea.style.textAlignLast);
|
|
23238
|
+
console.log(' direction:', this.textarea.style.direction);
|
|
23239
|
+
console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
|
|
23240
|
+
console.log(' width:', this.textarea.style.width);
|
|
23241
|
+
console.log(' textJustify:', this.textarea.style.textJustify);
|
|
23242
|
+
console.log(' wordSpacing:', this.textarea.style.wordSpacing);
|
|
23243
|
+
console.log(' whiteSpace:', this.textarea.style.whiteSpace);
|
|
23244
|
+
|
|
23245
|
+
// If justify, log Fabric object dimensions for comparison
|
|
23246
|
+
if (textAlign.includes('justify')) {
|
|
23247
|
+
var _calcTextWidth, _ref;
|
|
23248
|
+
console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
|
|
23249
|
+
console.log(' Fabric width:', target.width);
|
|
23250
|
+
console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
|
|
23251
|
+
console.log(' Fabric textAlign:', target.textAlign);
|
|
23252
|
+
console.log(' Text lines:', target.textLines);
|
|
23253
|
+
}
|
|
23254
|
+
|
|
23255
|
+
// Debug font properties matching
|
|
23256
|
+
console.log('🔤 FONT PROPERTIES COMPARISON:');
|
|
23257
|
+
console.log(' Fabric fontFamily:', target.fontFamily);
|
|
23258
|
+
console.log(' Fabric fontWeight:', target.fontWeight);
|
|
23259
|
+
console.log(' Fabric fontStyle:', target.fontStyle);
|
|
23260
|
+
console.log(' Fabric fontSize:', target.fontSize);
|
|
23261
|
+
console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
|
|
23262
|
+
console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
|
|
23263
|
+
console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
|
|
23264
|
+
console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
|
|
23265
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23266
|
+
|
|
23267
|
+
// Enhanced font rendering to better match fabric.js canvas rendering
|
|
23268
|
+
// Default to auto for more natural rendering
|
|
23269
|
+
this.textarea.style.webkitFontSmoothing = 'auto';
|
|
23270
|
+
this.textarea.style.mozOsxFontSmoothing = 'auto';
|
|
23271
|
+
this.textarea.style.fontSmooth = 'auto';
|
|
23272
|
+
this.textarea.style.textSizeAdjust = 'none';
|
|
23273
|
+
|
|
23274
|
+
// For bold fonts, use subpixel rendering to match canvas thickness better
|
|
23275
|
+
const fontWeight = String(target.fontWeight || 'normal');
|
|
23276
|
+
const isBold = fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 600;
|
|
23277
|
+
if (isBold) {
|
|
23278
|
+
this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
|
|
23279
|
+
this.textarea.style.mozOsxFontSmoothing = 'unset';
|
|
23280
|
+
console.log('🔤 Applied enhanced bold rendering for better thickness matching');
|
|
23281
|
+
}
|
|
23282
|
+
console.log('🎨 FONT SMOOTHING APPLIED:');
|
|
23283
|
+
console.log(' webkitFontSmoothing:', this.textarea.style.webkitFontSmoothing);
|
|
23284
|
+
console.log(' mozOsxFontSmoothing:', this.textarea.style.mozOsxFontSmoothing);
|
|
22187
23285
|
|
|
22188
23286
|
// Initial bounds are set correctly by Fabric.js - don't force update here
|
|
22189
23287
|
}
|
|
22190
23288
|
|
|
23289
|
+
/**
|
|
23290
|
+
* Debug method to compare textarea and canvas object bounding boxes
|
|
23291
|
+
*/
|
|
23292
|
+
debugBoundingBoxComparison() {
|
|
23293
|
+
const target = this.target;
|
|
23294
|
+
const canvas = this.canvas;
|
|
23295
|
+
const zoom = canvas.getZoom();
|
|
23296
|
+
|
|
23297
|
+
// Get textarea bounding box (in screen coordinates)
|
|
23298
|
+
const textareaRect = this.textarea.getBoundingClientRect();
|
|
23299
|
+
const hostRect = this.hostDiv.getBoundingClientRect();
|
|
23300
|
+
|
|
23301
|
+
// Get canvas object bounding box (in screen coordinates)
|
|
23302
|
+
const canvasBounds = target.getBoundingRect();
|
|
23303
|
+
const canvasRect = canvas.upperCanvasEl.getBoundingClientRect();
|
|
23304
|
+
|
|
23305
|
+
// Convert canvas object bounds to screen coordinates
|
|
23306
|
+
const vpt = canvas.viewportTransform;
|
|
23307
|
+
const screenObjectBounds = {
|
|
23308
|
+
left: canvasRect.left + canvasBounds.left * zoom + vpt[4],
|
|
23309
|
+
top: canvasRect.top + canvasBounds.top * zoom + vpt[5],
|
|
23310
|
+
width: canvasBounds.width * zoom,
|
|
23311
|
+
height: canvasBounds.height * zoom
|
|
23312
|
+
};
|
|
23313
|
+
console.log('🔍 BOUNDING BOX COMPARISON:');
|
|
23314
|
+
console.log('📦 Textarea Rect:', {
|
|
23315
|
+
left: Math.round(textareaRect.left * 100) / 100,
|
|
23316
|
+
top: Math.round(textareaRect.top * 100) / 100,
|
|
23317
|
+
width: Math.round(textareaRect.width * 100) / 100,
|
|
23318
|
+
height: Math.round(textareaRect.height * 100) / 100
|
|
23319
|
+
});
|
|
23320
|
+
console.log('📦 Host Div Rect:', {
|
|
23321
|
+
left: Math.round(hostRect.left * 100) / 100,
|
|
23322
|
+
top: Math.round(hostRect.top * 100) / 100,
|
|
23323
|
+
width: Math.round(hostRect.width * 100) / 100,
|
|
23324
|
+
height: Math.round(hostRect.height * 100) / 100
|
|
23325
|
+
});
|
|
23326
|
+
console.log('📦 Canvas Object Bounds (screen):', {
|
|
23327
|
+
left: Math.round(screenObjectBounds.left * 100) / 100,
|
|
23328
|
+
top: Math.round(screenObjectBounds.top * 100) / 100,
|
|
23329
|
+
width: Math.round(screenObjectBounds.width * 100) / 100,
|
|
23330
|
+
height: Math.round(screenObjectBounds.height * 100) / 100
|
|
23331
|
+
});
|
|
23332
|
+
console.log('📦 Canvas Object Bounds (canvas):', canvasBounds);
|
|
23333
|
+
|
|
23334
|
+
// Calculate differences
|
|
23335
|
+
const hostVsObject = {
|
|
23336
|
+
leftDiff: Math.round((hostRect.left - screenObjectBounds.left) * 100) / 100,
|
|
23337
|
+
topDiff: Math.round((hostRect.top - screenObjectBounds.top) * 100) / 100,
|
|
23338
|
+
widthDiff: Math.round((hostRect.width - screenObjectBounds.width) * 100) / 100,
|
|
23339
|
+
heightDiff: Math.round((hostRect.height - screenObjectBounds.height) * 100) / 100
|
|
23340
|
+
};
|
|
23341
|
+
const textareaVsObject = {
|
|
23342
|
+
leftDiff: Math.round((textareaRect.left - screenObjectBounds.left) * 100) / 100,
|
|
23343
|
+
topDiff: Math.round((textareaRect.top - screenObjectBounds.top) * 100) / 100,
|
|
23344
|
+
widthDiff: Math.round((textareaRect.width - screenObjectBounds.width) * 100) / 100,
|
|
23345
|
+
heightDiff: Math.round((textareaRect.height - screenObjectBounds.height) * 100) / 100
|
|
23346
|
+
};
|
|
23347
|
+
console.log('📏 Host Div vs Canvas Object Diff:', hostVsObject);
|
|
23348
|
+
console.log('📏 Textarea vs Canvas Object Diff:', textareaVsObject);
|
|
23349
|
+
|
|
23350
|
+
// Check if they're aligned (within 2px tolerance)
|
|
23351
|
+
const tolerance = 2;
|
|
23352
|
+
const hostAligned = Math.abs(hostVsObject.leftDiff) < tolerance && Math.abs(hostVsObject.topDiff) < tolerance && Math.abs(hostVsObject.widthDiff) < tolerance && Math.abs(hostVsObject.heightDiff) < tolerance;
|
|
23353
|
+
const textareaAligned = Math.abs(textareaVsObject.leftDiff) < tolerance && Math.abs(textareaVsObject.topDiff) < tolerance && Math.abs(textareaVsObject.widthDiff) < tolerance && Math.abs(textareaVsObject.heightDiff) < tolerance;
|
|
23354
|
+
console.log(hostAligned ? '✅ Host Div ALIGNED with canvas object' : '❌ Host Div MISALIGNED with canvas object');
|
|
23355
|
+
console.log(textareaAligned ? '✅ Textarea ALIGNED with canvas object' : '❌ Textarea MISALIGNED with canvas object');
|
|
23356
|
+
console.log('🔍 Zoom:', zoom, 'Viewport Transform:', vpt);
|
|
23357
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23358
|
+
}
|
|
23359
|
+
|
|
23360
|
+
/**
|
|
23361
|
+
* Debug method to compare text wrapping between textarea and Fabric text object
|
|
23362
|
+
*/
|
|
23363
|
+
debugTextWrapping() {
|
|
23364
|
+
const target = this.target;
|
|
23365
|
+
const text = this.textarea.value;
|
|
23366
|
+
console.log('📝 TEXT WRAPPING COMPARISON:');
|
|
23367
|
+
console.log('📄 Text Content:', `"${text}"`);
|
|
23368
|
+
console.log('📄 Text Length:', text.length);
|
|
23369
|
+
|
|
23370
|
+
// Analyze line breaks
|
|
23371
|
+
const explicitLines = text.split('\n');
|
|
23372
|
+
console.log('📄 Explicit Lines (\\n):', explicitLines.length);
|
|
23373
|
+
explicitLines.forEach((line, i) => {
|
|
23374
|
+
console.log(` Line ${i + 1}: "${line}" (${line.length} chars)`);
|
|
23375
|
+
});
|
|
23376
|
+
|
|
23377
|
+
// Get textarea computed styles for wrapping analysis
|
|
23378
|
+
const textareaStyles = window.getComputedStyle(this.textarea);
|
|
23379
|
+
console.log('📐 Textarea Wrapping Styles:');
|
|
23380
|
+
console.log(' width:', textareaStyles.width);
|
|
23381
|
+
console.log(' fontSize:', textareaStyles.fontSize);
|
|
23382
|
+
console.log(' fontFamily:', textareaStyles.fontFamily);
|
|
23383
|
+
console.log(' fontWeight:', textareaStyles.fontWeight);
|
|
23384
|
+
console.log(' letterSpacing:', textareaStyles.letterSpacing);
|
|
23385
|
+
console.log(' lineHeight:', textareaStyles.lineHeight);
|
|
23386
|
+
console.log(' whiteSpace:', textareaStyles.whiteSpace);
|
|
23387
|
+
console.log(' wordWrap:', textareaStyles.wordWrap);
|
|
23388
|
+
console.log(' overflowWrap:', textareaStyles.overflowWrap);
|
|
23389
|
+
console.log(' direction:', textareaStyles.direction);
|
|
23390
|
+
console.log(' textAlign:', textareaStyles.textAlign);
|
|
23391
|
+
|
|
23392
|
+
// Get Fabric text object properties for comparison
|
|
23393
|
+
console.log('📐 Fabric Text Object Properties:');
|
|
23394
|
+
console.log(' width:', target.width);
|
|
23395
|
+
console.log(' fontSize:', target.fontSize);
|
|
23396
|
+
console.log(' fontFamily:', target.fontFamily);
|
|
23397
|
+
console.log(' fontWeight:', target.fontWeight);
|
|
23398
|
+
console.log(' charSpacing:', target.charSpacing);
|
|
23399
|
+
console.log(' lineHeight:', target.lineHeight);
|
|
23400
|
+
console.log(' direction:', target.direction);
|
|
23401
|
+
console.log(' textAlign:', target.textAlign);
|
|
23402
|
+
console.log(' scaleX:', target.scaleX);
|
|
23403
|
+
console.log(' scaleY:', target.scaleY);
|
|
23404
|
+
|
|
23405
|
+
// Calculate effective dimensions for comparison - use actual rendered width
|
|
23406
|
+
// **THE FIX:** Use getBoundingRect to get the *actual rendered width* of the Fabric object.
|
|
23407
|
+
const fabricEffectiveWidth = this.target.getBoundingRect().width;
|
|
23408
|
+
// Use the exact width set on textarea for comparison
|
|
23409
|
+
const textareaComputedWidth = parseFloat(window.getComputedStyle(this.textarea).width);
|
|
23410
|
+
const textareaEffectiveWidth = textareaComputedWidth / this.canvas.getZoom();
|
|
23411
|
+
const widthDiff = Math.abs(textareaEffectiveWidth - fabricEffectiveWidth);
|
|
23412
|
+
console.log('📏 Effective Width Comparison:');
|
|
23413
|
+
console.log(' Textarea Effective Width:', textareaEffectiveWidth);
|
|
23414
|
+
console.log(' Fabric Effective Width:', fabricEffectiveWidth);
|
|
23415
|
+
console.log(' Width Difference:', widthDiff.toFixed(2) + 'px');
|
|
23416
|
+
console.log(widthDiff < 1 ? '✅ Widths MATCH for wrapping' : '❌ Width MISMATCH may cause different wrapping');
|
|
23417
|
+
|
|
23418
|
+
// Check text direction and bidi handling
|
|
23419
|
+
const hasRTLText = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
23420
|
+
const hasBidiText = /[\u0590-\u06FF]/.test(text) && /[a-zA-Z]/.test(text);
|
|
23421
|
+
console.log('🌍 Text Direction Analysis:');
|
|
23422
|
+
console.log(' Has RTL characters:', hasRTLText);
|
|
23423
|
+
console.log(' Has mixed Bidi text:', hasBidiText);
|
|
23424
|
+
console.log(' Textarea direction:', textareaStyles.direction);
|
|
23425
|
+
console.log(' Fabric direction:', target.direction || 'auto');
|
|
23426
|
+
console.log(' Textarea unicodeBidi:', textareaStyles.unicodeBidi);
|
|
23427
|
+
|
|
23428
|
+
// Measure actual rendered line count
|
|
23429
|
+
const textareaScrollHeight = this.textarea.scrollHeight;
|
|
23430
|
+
const textareaLineHeight = parseFloat(textareaStyles.lineHeight) || parseFloat(textareaStyles.fontSize) * 1.2;
|
|
23431
|
+
const estimatedTextareaLines = Math.round(textareaScrollHeight / textareaLineHeight);
|
|
23432
|
+
console.log('📊 Line Count Analysis:');
|
|
23433
|
+
console.log(' Textarea scrollHeight:', textareaScrollHeight);
|
|
23434
|
+
console.log(' Textarea lineHeight:', textareaLineHeight);
|
|
23435
|
+
console.log(' Estimated rendered lines:', estimatedTextareaLines);
|
|
23436
|
+
console.log(' Explicit line breaks:', explicitLines.length);
|
|
23437
|
+
if (estimatedTextareaLines > explicitLines.length) {
|
|
23438
|
+
console.log('🔄 Text wrapping detected in textarea');
|
|
23439
|
+
console.log(' Wrapped lines:', estimatedTextareaLines - explicitLines.length);
|
|
23440
|
+
} else {
|
|
23441
|
+
console.log('📏 No text wrapping in textarea');
|
|
23442
|
+
}
|
|
23443
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
23444
|
+
}
|
|
23445
|
+
|
|
22191
23446
|
/**
|
|
22192
23447
|
* Focus the textarea and position cursor at end
|
|
22193
23448
|
*/
|
|
@@ -22216,6 +23471,11 @@ class OverlayEditor {
|
|
|
22216
23471
|
this.canvas.requestRenderAll();
|
|
22217
23472
|
this.target.setCoords();
|
|
22218
23473
|
this.applyOverlayStyle();
|
|
23474
|
+
|
|
23475
|
+
// Fix character mapping issues after JSON loading for browser-wrapped fonts
|
|
23476
|
+
if (this.target._fixCharacterMappingAfterJsonLoad) {
|
|
23477
|
+
this.target._fixCharacterMappingAfterJsonLoad();
|
|
23478
|
+
}
|
|
22219
23479
|
this.textarea.focus();
|
|
22220
23480
|
this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
|
|
22221
23481
|
|
|
@@ -22261,6 +23521,23 @@ class OverlayEditor {
|
|
|
22261
23521
|
// Handle commit/cancel after restoring visibility
|
|
22262
23522
|
if (commit && !this.isComposing) {
|
|
22263
23523
|
const finalText = this.textarea.value;
|
|
23524
|
+
|
|
23525
|
+
// Auto-detect text direction and update fabric object if needed
|
|
23526
|
+
const detectedDirection = this.firstStrongDir(finalText);
|
|
23527
|
+
const currentDirection = this.target.direction || 'ltr';
|
|
23528
|
+
if (detectedDirection && detectedDirection !== currentDirection) {
|
|
23529
|
+
console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
|
|
23530
|
+
console.log(` Text content: "${finalText.substring(0, 50)}..."`);
|
|
23531
|
+
|
|
23532
|
+
// Update the fabric object's direction
|
|
23533
|
+
this.target.set('direction', detectedDirection);
|
|
23534
|
+
|
|
23535
|
+
// Force a re-render to apply the direction change
|
|
23536
|
+
this.canvas.requestRenderAll();
|
|
23537
|
+
console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
|
|
23538
|
+
} else {
|
|
23539
|
+
console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
|
|
23540
|
+
}
|
|
22264
23541
|
if (this.onCommit) {
|
|
22265
23542
|
this.onCommit(finalText);
|
|
22266
23543
|
}
|
|
@@ -22300,25 +23577,40 @@ class OverlayEditor {
|
|
|
22300
23577
|
}
|
|
22301
23578
|
}
|
|
22302
23579
|
autoResizeTextarea() {
|
|
22303
|
-
//
|
|
22304
|
-
const
|
|
22305
|
-
|
|
22306
|
-
|
|
22307
|
-
|
|
23580
|
+
// Store the scroll position and the container's old height for comparison.
|
|
23581
|
+
const scrollTop = this.textarea.scrollTop;
|
|
23582
|
+
const oldHeight = parseFloat(this.hostDiv.style.height || '0');
|
|
23583
|
+
|
|
23584
|
+
// 1. **Force a reliable reflow.**
|
|
23585
|
+
// First, reset the textarea's height to a minimal value. This is the crucial step
|
|
23586
|
+
// that forces the browser to recalculate the content's height from scratch,
|
|
23587
|
+
// ignoring the hostDiv's larger, stale height.
|
|
23588
|
+
this.textarea.style.height = '1px';
|
|
23589
|
+
|
|
23590
|
+
// 2. Read the now-accurate scrollHeight. This value reflects the minimum
|
|
23591
|
+
// height required for the content, whether it's single or multi-line.
|
|
22308
23592
|
const scrollHeight = this.textarea.scrollHeight;
|
|
22309
23593
|
|
|
22310
|
-
//
|
|
22311
|
-
const
|
|
22312
|
-
const newHeight =
|
|
22313
|
-
const heightChanged = Math.abs(newHeight - oldHeight) > 2; // Only if meaningful change
|
|
23594
|
+
// A small buffer for rendering consistency across browsers.
|
|
23595
|
+
const buffer = 2;
|
|
23596
|
+
const newHeight = scrollHeight + buffer;
|
|
22314
23597
|
|
|
22315
|
-
|
|
22316
|
-
|
|
23598
|
+
// Check if the height has changed significantly.
|
|
23599
|
+
const heightChanged = Math.abs(newHeight - oldHeight) > 1;
|
|
22317
23600
|
|
|
22318
|
-
// Only update object bounds if
|
|
23601
|
+
// 4. Only update heights and object bounds if there was a change.
|
|
22319
23602
|
if (heightChanged) {
|
|
23603
|
+
this.textarea.style.height = `${newHeight}px`;
|
|
23604
|
+
this.hostDiv.style.height = `${newHeight}px`;
|
|
22320
23605
|
this.updateObjectBounds();
|
|
23606
|
+
} else {
|
|
23607
|
+
// If no significant change, ensure the textarea's height matches the container
|
|
23608
|
+
// to prevent any minor visual misalignment.
|
|
23609
|
+
this.textarea.style.height = this.hostDiv.style.height;
|
|
22321
23610
|
}
|
|
23611
|
+
|
|
23612
|
+
// 5. Restore the original scroll position.
|
|
23613
|
+
this.textarea.scrollTop = scrollTop;
|
|
22322
23614
|
}
|
|
22323
23615
|
handleKeyDown(e) {
|
|
22324
23616
|
if (e.key === 'Escape') {
|
|
@@ -22327,6 +23619,19 @@ class OverlayEditor {
|
|
|
22327
23619
|
} else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
22328
23620
|
e.preventDefault();
|
|
22329
23621
|
this.destroy(true); // Commit
|
|
23622
|
+
} else if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
|
|
23623
|
+
// For keys that might change the height, schedule a resize check
|
|
23624
|
+
// Use both immediate and delayed checks to catch all scenarios
|
|
23625
|
+
requestAnimationFrame(() => {
|
|
23626
|
+
if (!this.isDestroyed) {
|
|
23627
|
+
this.autoResizeTextarea();
|
|
23628
|
+
}
|
|
23629
|
+
});
|
|
23630
|
+
setTimeout(() => {
|
|
23631
|
+
if (!this.isDestroyed) {
|
|
23632
|
+
this.autoResizeTextarea();
|
|
23633
|
+
}
|
|
23634
|
+
}, 10); // Small delay to ensure DOM is updated
|
|
22330
23635
|
}
|
|
22331
23636
|
}
|
|
22332
23637
|
handleFocus() {
|
|
@@ -25303,6 +26608,7 @@ class Textbox extends IText {
|
|
|
25303
26608
|
...Textbox.ownDefaults,
|
|
25304
26609
|
...options
|
|
25305
26610
|
});
|
|
26611
|
+
this.initializeEventListeners();
|
|
25306
26612
|
}
|
|
25307
26613
|
|
|
25308
26614
|
/**
|
|
@@ -25324,8 +26630,27 @@ class Textbox extends IText {
|
|
|
25324
26630
|
*/
|
|
25325
26631
|
initDimensions() {
|
|
25326
26632
|
if (!this.initialized) {
|
|
26633
|
+
this.initialized = true;
|
|
26634
|
+
}
|
|
26635
|
+
|
|
26636
|
+
// Prevent rapid recalculations during moves
|
|
26637
|
+
if (this._usingBrowserWrapping) {
|
|
26638
|
+
const now = Date.now();
|
|
26639
|
+
const lastCall = this._lastInitDimensionsTime || 0;
|
|
26640
|
+
const isRapidCall = now - lastCall < 100;
|
|
26641
|
+
const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
|
|
26642
|
+
if (isRapidCall && !isDuringLoading) {
|
|
26643
|
+
return;
|
|
26644
|
+
}
|
|
26645
|
+
this._lastInitDimensionsTime = now;
|
|
26646
|
+
}
|
|
26647
|
+
|
|
26648
|
+
// Skip if nothing changed
|
|
26649
|
+
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
26650
|
+
if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
|
|
25327
26651
|
return;
|
|
25328
26652
|
}
|
|
26653
|
+
this._lastDimensionState = currentState;
|
|
25329
26654
|
|
|
25330
26655
|
// Use advanced layout if enabled
|
|
25331
26656
|
if (this.enableAdvancedLayout) {
|
|
@@ -25336,17 +26661,142 @@ class Textbox extends IText {
|
|
|
25336
26661
|
// clear dynamicMinWidth as it will be different after we re-wrap line
|
|
25337
26662
|
this.dynamicMinWidth = 0;
|
|
25338
26663
|
// wrap lines
|
|
25339
|
-
|
|
25340
|
-
|
|
25341
|
-
|
|
26664
|
+
const splitTextResult = this._splitText();
|
|
26665
|
+
this._styleMap = this._generateStyleMap(splitTextResult);
|
|
26666
|
+
|
|
26667
|
+
// For browser wrapping, ensure _textLines is set from browser results
|
|
26668
|
+
if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
|
|
26669
|
+
this._textLines = splitTextResult.lines.map(line => line.split(''));
|
|
26670
|
+
|
|
26671
|
+
// Store justify measurements and browser height
|
|
26672
|
+
const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
|
|
26673
|
+
if (justifyMeasurements) {
|
|
26674
|
+
this._styleMap.justifySpaceMeasurements = justifyMeasurements;
|
|
26675
|
+
}
|
|
26676
|
+
const actualHeight = splitTextResult.actualBrowserHeight;
|
|
26677
|
+
if (actualHeight) {
|
|
26678
|
+
this._actualBrowserHeight = actualHeight;
|
|
26679
|
+
}
|
|
26680
|
+
}
|
|
26681
|
+
// Don't auto-resize width when using browser wrapping to prevent width increases during moves
|
|
26682
|
+
if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
|
|
25342
26683
|
this._set('width', this.dynamicMinWidth);
|
|
25343
26684
|
}
|
|
26685
|
+
|
|
26686
|
+
// For browser wrapping fonts (like STV), ensure minimum width for new textboxes
|
|
26687
|
+
// since these fonts can't measure English characters properly
|
|
26688
|
+
if (this._usingBrowserWrapping && this.width < 50) {
|
|
26689
|
+
console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
|
|
26690
|
+
this.width = 300;
|
|
26691
|
+
}
|
|
26692
|
+
|
|
26693
|
+
// Mark browser wrapping as initialized when complete
|
|
26694
|
+
if (this._usingBrowserWrapping) {
|
|
26695
|
+
this._browserWrapInitialized = true;
|
|
26696
|
+
}
|
|
25344
26697
|
if (this.textAlign.includes(JUSTIFY)) {
|
|
26698
|
+
// For browser wrapping fonts, apply browser-calculated justify spaces
|
|
26699
|
+
if (this._usingBrowserWrapping) {
|
|
26700
|
+
console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
|
|
26701
|
+
this._applyBrowserJustifySpaces();
|
|
26702
|
+
return;
|
|
26703
|
+
}
|
|
26704
|
+
|
|
26705
|
+
// Don't apply justify alignment during drag operations to prevent snapping
|
|
26706
|
+
const now = Date.now();
|
|
26707
|
+
const lastDragTime = this._lastInitDimensionsTime || 0;
|
|
26708
|
+
const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
|
|
26709
|
+
|
|
26710
|
+
if (isDuringDrag) {
|
|
26711
|
+
console.log('🔤 Skipping justify during drag operation to prevent snapping');
|
|
26712
|
+
return;
|
|
26713
|
+
}
|
|
26714
|
+
|
|
26715
|
+
// For non-browser-wrapping fonts, use Fabric's justify system
|
|
25345
26716
|
// once text is measured we need to make space fatter to make justified text.
|
|
25346
|
-
|
|
26717
|
+
// Ensure __charBounds exists and fonts are ready before applying justify
|
|
26718
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
26719
|
+
// Check if font is ready for accurate justify calculations
|
|
26720
|
+
const fontReady = this._isFontReady ? this._isFontReady() : true;
|
|
26721
|
+
if (fontReady) {
|
|
26722
|
+
this.enlargeSpaces();
|
|
26723
|
+
} else {
|
|
26724
|
+
console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
|
|
26725
|
+
// Defer justify calculation until font is ready
|
|
26726
|
+
this._scheduleJustifyAfterFontLoad();
|
|
26727
|
+
}
|
|
26728
|
+
} else {
|
|
26729
|
+
console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
|
|
26730
|
+
// Defer the justify calculation until the next frame
|
|
26731
|
+
setTimeout(() => {
|
|
26732
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
26733
|
+
var _this$canvas;
|
|
26734
|
+
console.log('🔧 Applying deferred Textbox justify alignment');
|
|
26735
|
+
this.enlargeSpaces();
|
|
26736
|
+
(_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
|
|
26737
|
+
}
|
|
26738
|
+
}, 0);
|
|
26739
|
+
}
|
|
26740
|
+
}
|
|
26741
|
+
// Calculate height - use Fabric's calculation for proper text rendering space
|
|
26742
|
+
if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
|
|
26743
|
+
const actualBrowserHeight = this._actualBrowserHeight;
|
|
26744
|
+
const oldHeight = this.height;
|
|
26745
|
+
// Use Fabric's height calculation since it knows how much space text rendering needs
|
|
26746
|
+
this.height = this.calcTextHeight();
|
|
26747
|
+
|
|
26748
|
+
// Force canvas refresh and control update if height changed significantly
|
|
26749
|
+
if (Math.abs(this.height - oldHeight) > 1) {
|
|
26750
|
+
var _this$canvas2, _this$_textLines;
|
|
26751
|
+
this.setCoords();
|
|
26752
|
+
(_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
|
|
26753
|
+
|
|
26754
|
+
// DEBUG: Log exact positioning details
|
|
26755
|
+
console.log(`🎯 POSITIONING DEBUG:`);
|
|
26756
|
+
console.log(` Textbox height: ${this.height}px`);
|
|
26757
|
+
console.log(` Textbox top: ${this.top}px`);
|
|
26758
|
+
console.log(` Textbox left: ${this.left}px`);
|
|
26759
|
+
console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
|
|
26760
|
+
console.log(` Font size: ${this.fontSize}px`);
|
|
26761
|
+
console.log(` Line height: ${this.lineHeight || 1.16}`);
|
|
26762
|
+
console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
|
|
26763
|
+
console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
|
|
26764
|
+
console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
|
|
26765
|
+
console.log(` Browser height: ${actualBrowserHeight}px`);
|
|
26766
|
+
console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
|
|
26767
|
+
}
|
|
26768
|
+
} else {
|
|
26769
|
+
this.height = this.calcTextHeight();
|
|
26770
|
+
}
|
|
26771
|
+
}
|
|
26772
|
+
|
|
26773
|
+
/**
|
|
26774
|
+
* Schedule justify calculation after font loads (Textbox-specific)
|
|
26775
|
+
* @private
|
|
26776
|
+
*/
|
|
26777
|
+
_scheduleJustifyAfterFontLoad() {
|
|
26778
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
26779
|
+
return;
|
|
25347
26780
|
}
|
|
25348
|
-
|
|
25349
|
-
|
|
26781
|
+
|
|
26782
|
+
// Only schedule if not already waiting
|
|
26783
|
+
if (this._fontJustifyScheduled) {
|
|
26784
|
+
return;
|
|
26785
|
+
}
|
|
26786
|
+
this._fontJustifyScheduled = true;
|
|
26787
|
+
const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
|
|
26788
|
+
document.fonts.load(fontSpec).then(() => {
|
|
26789
|
+
var _this$canvas3;
|
|
26790
|
+
this._fontJustifyScheduled = false;
|
|
26791
|
+
console.log('🔧 Textbox: Font loaded, applying justify alignment');
|
|
26792
|
+
|
|
26793
|
+
// Re-run initDimensions to ensure proper justify calculation
|
|
26794
|
+
this.initDimensions();
|
|
26795
|
+
(_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
|
|
26796
|
+
}).catch(() => {
|
|
26797
|
+
this._fontJustifyScheduled = false;
|
|
26798
|
+
console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
|
|
26799
|
+
});
|
|
25350
26800
|
}
|
|
25351
26801
|
|
|
25352
26802
|
/**
|
|
@@ -25713,19 +27163,33 @@ class Textbox extends IText {
|
|
|
25713
27163
|
width: wordWidth
|
|
25714
27164
|
} = data[i];
|
|
25715
27165
|
offset += word.length;
|
|
25716
|
-
|
|
25717
|
-
if
|
|
27166
|
+
|
|
27167
|
+
// Predictive wrapping: check if adding this word would exceed the width
|
|
27168
|
+
const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
|
|
27169
|
+
// Use exact width to match overlay editor behavior
|
|
27170
|
+
const conservativeMaxWidth = maxWidth; // No artificial buffer
|
|
27171
|
+
|
|
27172
|
+
// Debug logging for wrapping decisions
|
|
27173
|
+
const currentLineText = line.join('');
|
|
27174
|
+
console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
|
|
27175
|
+
if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
|
|
27176
|
+
// This word would exceed the width, wrap before adding it
|
|
27177
|
+
console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
|
|
25718
27178
|
graphemeLines.push(line);
|
|
25719
27179
|
line = [];
|
|
25720
|
-
lineWidth = wordWidth;
|
|
27180
|
+
lineWidth = wordWidth; // Start new line with just this word
|
|
25721
27181
|
lineJustStarted = true;
|
|
25722
27182
|
} else {
|
|
25723
|
-
|
|
27183
|
+
// Word fits, add it to current line
|
|
27184
|
+
lineWidth = potentialLineWidth + additionalSpace;
|
|
25724
27185
|
}
|
|
25725
27186
|
if (!lineJustStarted && !splitByGrapheme) {
|
|
25726
27187
|
line.push(infix);
|
|
25727
27188
|
}
|
|
25728
27189
|
line = line.concat(word);
|
|
27190
|
+
|
|
27191
|
+
// Debug: show current line after adding word
|
|
27192
|
+
console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
|
|
25729
27193
|
infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
|
|
25730
27194
|
offset++;
|
|
25731
27195
|
lineJustStarted = false;
|
|
@@ -25735,9 +27199,19 @@ class Textbox extends IText {
|
|
|
25735
27199
|
// TODO: this code is probably not necessary anymore.
|
|
25736
27200
|
// it can be moved out of this function since largestWordWidth is now
|
|
25737
27201
|
// known in advance
|
|
25738
|
-
|
|
27202
|
+
// Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
|
|
27203
|
+
if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
|
|
27204
|
+
console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
|
|
25739
27205
|
this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
|
|
27206
|
+
} else if (this._usingBrowserWrapping) {
|
|
27207
|
+
console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
|
|
25740
27208
|
}
|
|
27209
|
+
|
|
27210
|
+
// Debug: show final wrapped lines
|
|
27211
|
+
console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
|
|
27212
|
+
graphemeLines.forEach((line, i) => {
|
|
27213
|
+
console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
|
|
27214
|
+
});
|
|
25741
27215
|
return graphemeLines;
|
|
25742
27216
|
}
|
|
25743
27217
|
|
|
@@ -25781,6 +27255,260 @@ class Textbox extends IText {
|
|
|
25781
27255
|
* @override
|
|
25782
27256
|
*/
|
|
25783
27257
|
_splitTextIntoLines(text) {
|
|
27258
|
+
// Check if we need browser wrapping using smart font detection
|
|
27259
|
+
const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
|
|
27260
|
+
if (needsBrowserWrapping) {
|
|
27261
|
+
// Cache key based on text content, width, font properties, AND text alignment
|
|
27262
|
+
const textHash = text.length + text.slice(0, 50); // Include text content in cache key
|
|
27263
|
+
const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
27264
|
+
|
|
27265
|
+
// Check if we have a cached result and nothing has changed
|
|
27266
|
+
if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
|
|
27267
|
+
const cachedResult = this._browserWrapCache.result;
|
|
27268
|
+
|
|
27269
|
+
// For justify alignment, ensure we have the measurements
|
|
27270
|
+
if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
|
|
27271
|
+
return cachedResult;
|
|
27272
|
+
}
|
|
27273
|
+
}
|
|
27274
|
+
const result = this._splitTextIntoLinesWithBrowser(text);
|
|
27275
|
+
|
|
27276
|
+
// Cache the result
|
|
27277
|
+
this._browserWrapCache = {
|
|
27278
|
+
key: cacheKey,
|
|
27279
|
+
result
|
|
27280
|
+
};
|
|
27281
|
+
|
|
27282
|
+
// Mark that we used browser wrapping to prevent dynamicMinWidth modifications
|
|
27283
|
+
this._usingBrowserWrapping = true;
|
|
27284
|
+
return result;
|
|
27285
|
+
}
|
|
27286
|
+
|
|
27287
|
+
// Clear the browser wrapping flag when using regular wrapping
|
|
27288
|
+
this._usingBrowserWrapping = false;
|
|
27289
|
+
|
|
27290
|
+
// Default Fabric wrapping for other fonts
|
|
27291
|
+
const newText = super._splitTextIntoLines(text),
|
|
27292
|
+
graphemeLines = this._wrapText(newText.lines, this.width),
|
|
27293
|
+
lines = new Array(graphemeLines.length);
|
|
27294
|
+
for (let i = 0; i < graphemeLines.length; i++) {
|
|
27295
|
+
lines[i] = graphemeLines[i].join('');
|
|
27296
|
+
}
|
|
27297
|
+
newText.lines = lines;
|
|
27298
|
+
newText.graphemeLines = graphemeLines;
|
|
27299
|
+
return newText;
|
|
27300
|
+
}
|
|
27301
|
+
|
|
27302
|
+
/**
|
|
27303
|
+
* Use browser's native text wrapping for accurate handling of fonts without English glyphs
|
|
27304
|
+
* @private
|
|
27305
|
+
*/
|
|
27306
|
+
_splitTextIntoLinesWithBrowser(text) {
|
|
27307
|
+
if (typeof document === 'undefined') {
|
|
27308
|
+
// Fallback to regular wrapping in Node.js
|
|
27309
|
+
return this._splitTextIntoLinesDefault(text);
|
|
27310
|
+
}
|
|
27311
|
+
|
|
27312
|
+
// Create a hidden element that mimics the overlay editor
|
|
27313
|
+
const testElement = document.createElement('div');
|
|
27314
|
+
testElement.style.position = 'absolute';
|
|
27315
|
+
testElement.style.left = '-9999px';
|
|
27316
|
+
testElement.style.visibility = 'hidden';
|
|
27317
|
+
testElement.style.fontSize = `${this.fontSize}px`;
|
|
27318
|
+
testElement.style.fontFamily = `"${this.fontFamily}"`;
|
|
27319
|
+
testElement.style.fontWeight = String(this.fontWeight || 'normal');
|
|
27320
|
+
testElement.style.fontStyle = String(this.fontStyle || 'normal');
|
|
27321
|
+
testElement.style.lineHeight = String(this.lineHeight || 1.16);
|
|
27322
|
+
testElement.style.width = `${this.width}px`;
|
|
27323
|
+
testElement.style.direction = this.direction || 'ltr';
|
|
27324
|
+
testElement.style.whiteSpace = 'pre-wrap';
|
|
27325
|
+
testElement.style.wordBreak = 'normal';
|
|
27326
|
+
testElement.style.overflowWrap = 'break-word';
|
|
27327
|
+
|
|
27328
|
+
// Set browser-native text alignment (including justify)
|
|
27329
|
+
if (this.textAlign.includes('justify')) {
|
|
27330
|
+
testElement.style.textAlign = 'justify';
|
|
27331
|
+
testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
|
|
27332
|
+
} else {
|
|
27333
|
+
testElement.style.textAlign = this.textAlign;
|
|
27334
|
+
}
|
|
27335
|
+
testElement.textContent = text;
|
|
27336
|
+
document.body.appendChild(testElement);
|
|
27337
|
+
|
|
27338
|
+
// Get the browser's natural line breaks
|
|
27339
|
+
const range = document.createRange();
|
|
27340
|
+
const lines = [];
|
|
27341
|
+
const graphemeLines = [];
|
|
27342
|
+
try {
|
|
27343
|
+
// Simple approach: split by measuring character positions
|
|
27344
|
+
const textNode = testElement.firstChild;
|
|
27345
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
27346
|
+
let currentLineStart = 0;
|
|
27347
|
+
const textLength = text.length;
|
|
27348
|
+
let previousBottom = 0;
|
|
27349
|
+
for (let i = 0; i <= textLength; i++) {
|
|
27350
|
+
range.setStart(textNode, currentLineStart);
|
|
27351
|
+
range.setEnd(textNode, i);
|
|
27352
|
+
const rect = range.getBoundingClientRect();
|
|
27353
|
+
if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
|
|
27354
|
+
// New line detected or end of text
|
|
27355
|
+
const lineEnd = i === textLength ? i : i - 1;
|
|
27356
|
+
const lineText = text.substring(currentLineStart, lineEnd).trim();
|
|
27357
|
+
if (lineText) {
|
|
27358
|
+
lines.push(lineText);
|
|
27359
|
+
// Convert to graphemes for compatibility
|
|
27360
|
+
const graphemeLine = lineText.split('');
|
|
27361
|
+
graphemeLines.push(graphemeLine);
|
|
27362
|
+
}
|
|
27363
|
+
currentLineStart = lineEnd;
|
|
27364
|
+
previousBottom = rect.bottom;
|
|
27365
|
+
}
|
|
27366
|
+
}
|
|
27367
|
+
}
|
|
27368
|
+
} catch (error) {
|
|
27369
|
+
console.warn('Browser wrapping failed, using fallback:', error);
|
|
27370
|
+
document.body.removeChild(testElement);
|
|
27371
|
+
return this._splitTextIntoLinesDefault(text);
|
|
27372
|
+
}
|
|
27373
|
+
|
|
27374
|
+
// Extract actual browser height BEFORE removing element
|
|
27375
|
+
const actualBrowserHeight = testElement.scrollHeight;
|
|
27376
|
+
const offsetHeight = testElement.offsetHeight;
|
|
27377
|
+
const clientHeight = testElement.clientHeight;
|
|
27378
|
+
const boundingRect = testElement.getBoundingClientRect();
|
|
27379
|
+
console.log(`🔤 Browser element measurements:`);
|
|
27380
|
+
console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
|
|
27381
|
+
console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
|
|
27382
|
+
console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
|
|
27383
|
+
console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
|
|
27384
|
+
console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
|
|
27385
|
+
|
|
27386
|
+
// For justify alignment, extract space measurements from browser BEFORE removing element
|
|
27387
|
+
let justifySpaceMeasurements = null;
|
|
27388
|
+
if (this.textAlign.includes('justify')) {
|
|
27389
|
+
justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
|
|
27390
|
+
}
|
|
27391
|
+
document.body.removeChild(testElement);
|
|
27392
|
+
console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
|
|
27393
|
+
|
|
27394
|
+
// Try different height measurements to find the most accurate
|
|
27395
|
+
let bestHeight = actualBrowserHeight;
|
|
27396
|
+
|
|
27397
|
+
// If scrollHeight and offsetHeight differ significantly, investigate
|
|
27398
|
+
if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
|
|
27399
|
+
console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
|
|
27400
|
+
}
|
|
27401
|
+
|
|
27402
|
+
// Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
|
|
27403
|
+
if (boundingRect.height > bestHeight) {
|
|
27404
|
+
console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
|
|
27405
|
+
bestHeight = boundingRect.height;
|
|
27406
|
+
}
|
|
27407
|
+
|
|
27408
|
+
// Font-specific height adjustments for accurate bounding box
|
|
27409
|
+
let adjustedHeight = bestHeight;
|
|
27410
|
+
|
|
27411
|
+
// Fonts without English glyphs need additional height buffer due to different font metrics
|
|
27412
|
+
const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
|
|
27413
|
+
if (lacksEnglishGlyphs) {
|
|
27414
|
+
const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
|
|
27415
|
+
adjustedHeight = bestHeight + glyphBuffer;
|
|
27416
|
+
console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
|
|
27417
|
+
} else {
|
|
27418
|
+
console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
|
|
27419
|
+
}
|
|
27420
|
+
return {
|
|
27421
|
+
_unwrappedLines: [text.split('')],
|
|
27422
|
+
lines: lines,
|
|
27423
|
+
graphemeText: text.split(''),
|
|
27424
|
+
graphemeLines: graphemeLines,
|
|
27425
|
+
justifySpaceMeasurements: justifySpaceMeasurements,
|
|
27426
|
+
actualBrowserHeight: adjustedHeight
|
|
27427
|
+
};
|
|
27428
|
+
}
|
|
27429
|
+
|
|
27430
|
+
/**
|
|
27431
|
+
* Extract justify space measurements from browser
|
|
27432
|
+
* @private
|
|
27433
|
+
*/
|
|
27434
|
+
_extractJustifySpaceMeasurements(element, lines) {
|
|
27435
|
+
console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
|
|
27436
|
+
|
|
27437
|
+
// For now, we'll use a simplified approach:
|
|
27438
|
+
// Apply uniform space expansion to match the line width
|
|
27439
|
+
const spaceWidths = [];
|
|
27440
|
+
lines.forEach((line, lineIndex) => {
|
|
27441
|
+
const lineSpaces = [];
|
|
27442
|
+
const spaceCount = (line.match(/\s/g) || []).length;
|
|
27443
|
+
if (spaceCount > 0 && lineIndex < lines.length - 1) {
|
|
27444
|
+
// Don't justify last line
|
|
27445
|
+
// Calculate how much space expansion is needed
|
|
27446
|
+
const normalSpaceWidth = 6.4; // Default space width for STV font
|
|
27447
|
+
const lineWidth = this.width;
|
|
27448
|
+
|
|
27449
|
+
// Estimate natural line width
|
|
27450
|
+
const charCount = line.length - spaceCount;
|
|
27451
|
+
const avgCharWidth = 12; // Approximate for STV font
|
|
27452
|
+
|
|
27453
|
+
// Calculate expanded space width
|
|
27454
|
+
const remainingSpace = lineWidth - charCount * avgCharWidth;
|
|
27455
|
+
const expandedSpaceWidth = remainingSpace / spaceCount;
|
|
27456
|
+
console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
|
|
27457
|
+
|
|
27458
|
+
// Fill array with expanded space widths for this line
|
|
27459
|
+
for (let i = 0; i < spaceCount; i++) {
|
|
27460
|
+
lineSpaces.push(expandedSpaceWidth);
|
|
27461
|
+
}
|
|
27462
|
+
}
|
|
27463
|
+
spaceWidths.push(lineSpaces);
|
|
27464
|
+
});
|
|
27465
|
+
return spaceWidths;
|
|
27466
|
+
}
|
|
27467
|
+
|
|
27468
|
+
/**
|
|
27469
|
+
* Apply browser-calculated justify space measurements
|
|
27470
|
+
* @private
|
|
27471
|
+
*/
|
|
27472
|
+
_applyBrowserJustifySpaces() {
|
|
27473
|
+
if (!this._textLines || !this.__charBounds) {
|
|
27474
|
+
console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
|
|
27475
|
+
return;
|
|
27476
|
+
}
|
|
27477
|
+
|
|
27478
|
+
// Get space measurements from browser wrapping result
|
|
27479
|
+
const styleMap = this._styleMap;
|
|
27480
|
+
if (!styleMap || !styleMap.justifySpaceMeasurements) {
|
|
27481
|
+
console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
|
|
27482
|
+
return;
|
|
27483
|
+
}
|
|
27484
|
+
const spaceWidths = styleMap.justifySpaceMeasurements;
|
|
27485
|
+
console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
|
|
27486
|
+
|
|
27487
|
+
// Apply space widths to character bounds
|
|
27488
|
+
this._textLines.forEach((line, lineIndex) => {
|
|
27489
|
+
if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
|
|
27490
|
+
const lineBounds = this.__charBounds[lineIndex];
|
|
27491
|
+
const lineSpaceWidths = spaceWidths[lineIndex];
|
|
27492
|
+
let spaceIndex = 0;
|
|
27493
|
+
for (let charIndex = 0; charIndex < line.length; charIndex++) {
|
|
27494
|
+
if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
|
|
27495
|
+
const expandedWidth = lineSpaceWidths[spaceIndex];
|
|
27496
|
+
if (lineBounds[charIndex]) {
|
|
27497
|
+
const oldWidth = lineBounds[charIndex].width;
|
|
27498
|
+
lineBounds[charIndex].width = expandedWidth;
|
|
27499
|
+
console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
|
|
27500
|
+
}
|
|
27501
|
+
spaceIndex++;
|
|
27502
|
+
}
|
|
27503
|
+
}
|
|
27504
|
+
});
|
|
27505
|
+
}
|
|
27506
|
+
|
|
27507
|
+
/**
|
|
27508
|
+
* Fallback to default Fabric wrapping
|
|
27509
|
+
* @private
|
|
27510
|
+
*/
|
|
27511
|
+
_splitTextIntoLinesDefault(text) {
|
|
25784
27512
|
const newText = super._splitTextIntoLines(text),
|
|
25785
27513
|
graphemeLines = this._wrapText(newText.lines, this.width),
|
|
25786
27514
|
lines = new Array(graphemeLines.length);
|
|
@@ -25810,6 +27538,204 @@ class Textbox extends IText {
|
|
|
25810
27538
|
}
|
|
25811
27539
|
}
|
|
25812
27540
|
|
|
27541
|
+
/**
|
|
27542
|
+
* Initialize event listeners for safety snap functionality
|
|
27543
|
+
* @private
|
|
27544
|
+
*/
|
|
27545
|
+
initializeEventListeners() {
|
|
27546
|
+
var _this$canvas4;
|
|
27547
|
+
// Track which side is being used for resize to handle position compensation
|
|
27548
|
+
let resizeOrigin = null;
|
|
27549
|
+
|
|
27550
|
+
// Detect resize origin during resizing
|
|
27551
|
+
this.on('resizing', e => {
|
|
27552
|
+
// Check transform origin to determine which side is being resized
|
|
27553
|
+
if (e.transform) {
|
|
27554
|
+
const {
|
|
27555
|
+
originX
|
|
27556
|
+
} = e.transform;
|
|
27557
|
+
// originX tells us which side is the anchor - opposite side is being dragged
|
|
27558
|
+
resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
|
|
27559
|
+
} else if (e.originX) {
|
|
27560
|
+
const {
|
|
27561
|
+
originX
|
|
27562
|
+
} = e;
|
|
27563
|
+
resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
|
|
27564
|
+
}
|
|
27565
|
+
});
|
|
27566
|
+
|
|
27567
|
+
// Only trigger safety snap after resize is complete (not during)
|
|
27568
|
+
// Use 'modified' event which fires after user releases the mouse
|
|
27569
|
+
this.on('modified', () => {
|
|
27570
|
+
const currentResizeOrigin = resizeOrigin; // Capture the value before reset
|
|
27571
|
+
// Small delay to ensure text layout is updated
|
|
27572
|
+
setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
|
|
27573
|
+
resizeOrigin = null; // Reset after capturing
|
|
27574
|
+
});
|
|
27575
|
+
|
|
27576
|
+
// Also listen to canvas-level modified event as backup
|
|
27577
|
+
(_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
|
|
27578
|
+
if (e.target === this) {
|
|
27579
|
+
const currentResizeOrigin = resizeOrigin; // Capture the value before reset
|
|
27580
|
+
setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
|
|
27581
|
+
resizeOrigin = null; // Reset after capturing
|
|
27582
|
+
}
|
|
27583
|
+
});
|
|
27584
|
+
}
|
|
27585
|
+
|
|
27586
|
+
/**
|
|
27587
|
+
* Safety snap to prevent glyph clipping after manual resize.
|
|
27588
|
+
* Similar to Polotno - checks if any glyphs are too close to edges
|
|
27589
|
+
* and automatically expands width if needed.
|
|
27590
|
+
* @private
|
|
27591
|
+
* @param resizeOrigin - Which side was used for resizing ('left' or 'right')
|
|
27592
|
+
*/
|
|
27593
|
+
safetySnapWidth(resizeOrigin) {
|
|
27594
|
+
// For Textbox objects, we always want to check for clipping regardless of isWrapping flag
|
|
27595
|
+
if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
|
|
27596
|
+
return;
|
|
27597
|
+
}
|
|
27598
|
+
const lineCount = this._textLines.length;
|
|
27599
|
+
if (lineCount === 0) return;
|
|
27600
|
+
let maxRequiredWidth = 0; // Width including RTL buffer
|
|
27601
|
+
|
|
27602
|
+
for (let i = 0; i < lineCount; i++) {
|
|
27603
|
+
const lineText = this._textLines[i].join(''); // Convert grapheme array to string
|
|
27604
|
+
const lineWidth = this.getLineWidth(i);
|
|
27605
|
+
|
|
27606
|
+
// RTL detection - regex for Arabic, Hebrew, and other RTL characters
|
|
27607
|
+
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
|
27608
|
+
if (rtlRegex.test(lineText)) {
|
|
27609
|
+
// Add minimal RTL compensation buffer - just enough to prevent clipping
|
|
27610
|
+
const rtlBuffer = (this.fontSize || 16) * 0.15; // 15% of font size (much smaller)
|
|
27611
|
+
maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth + rtlBuffer);
|
|
27612
|
+
} else {
|
|
27613
|
+
maxRequiredWidth = Math.max(maxRequiredWidth, lineWidth);
|
|
27614
|
+
}
|
|
27615
|
+
}
|
|
27616
|
+
|
|
27617
|
+
// Safety margin - how close glyphs can get before we snap
|
|
27618
|
+
const safetyThreshold = 2; // px - very subtle trigger
|
|
27619
|
+
|
|
27620
|
+
if (maxRequiredWidth > this.width - safetyThreshold) {
|
|
27621
|
+
var _this$canvas5;
|
|
27622
|
+
// Set width to exactly what's needed + minimal safety margin
|
|
27623
|
+
const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
|
|
27624
|
+
|
|
27625
|
+
// Store original position before width change
|
|
27626
|
+
const originalLeft = this.left;
|
|
27627
|
+
const originalTop = this.top;
|
|
27628
|
+
const widthIncrease = newWidth - this.width;
|
|
27629
|
+
|
|
27630
|
+
// Change width
|
|
27631
|
+
this.set('width', newWidth);
|
|
27632
|
+
|
|
27633
|
+
// Force text layout recalculation
|
|
27634
|
+
this.initDimensions();
|
|
27635
|
+
|
|
27636
|
+
// Only compensate position when resizing from left handle
|
|
27637
|
+
// Right handle resize doesn't shift the text position
|
|
27638
|
+
if (resizeOrigin === 'left') {
|
|
27639
|
+
// When resizing from left, the expansion pushes text right
|
|
27640
|
+
// Compensate by moving the textbox left by the width increase
|
|
27641
|
+
this.set({
|
|
27642
|
+
'left': originalLeft - widthIncrease,
|
|
27643
|
+
'top': originalTop
|
|
27644
|
+
});
|
|
27645
|
+
}
|
|
27646
|
+
this.setCoords();
|
|
27647
|
+
|
|
27648
|
+
// Also refresh the overlay editor if it exists
|
|
27649
|
+
if (this.__overlayEditor) {
|
|
27650
|
+
setTimeout(() => {
|
|
27651
|
+
this.__overlayEditor.refresh();
|
|
27652
|
+
}, 0);
|
|
27653
|
+
}
|
|
27654
|
+
(_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
|
|
27655
|
+
}
|
|
27656
|
+
}
|
|
27657
|
+
|
|
27658
|
+
/**
|
|
27659
|
+
* Fix character selection mismatch after JSON loading for browser-wrapped fonts
|
|
27660
|
+
* @private
|
|
27661
|
+
*/
|
|
27662
|
+
_fixCharacterMappingAfterJsonLoad() {
|
|
27663
|
+
if (this._usingBrowserWrapping) {
|
|
27664
|
+
// Clear all cached states to force fresh text layout calculation
|
|
27665
|
+
this._browserWrapCache = null;
|
|
27666
|
+
this._lastDimensionState = null;
|
|
27667
|
+
|
|
27668
|
+
// Force complete re-initialization
|
|
27669
|
+
this.initDimensions();
|
|
27670
|
+
this._forceClearCache = true;
|
|
27671
|
+
|
|
27672
|
+
// Ensure canvas refresh
|
|
27673
|
+
this.setCoords();
|
|
27674
|
+
if (this.canvas) {
|
|
27675
|
+
this.canvas.requestRenderAll();
|
|
27676
|
+
}
|
|
27677
|
+
}
|
|
27678
|
+
}
|
|
27679
|
+
|
|
27680
|
+
/**
|
|
27681
|
+
* Force complete textbox re-initialization (useful after JSON loading)
|
|
27682
|
+
* Overrides Text version with Textbox-specific logic
|
|
27683
|
+
*/
|
|
27684
|
+
forceTextReinitialization() {
|
|
27685
|
+
console.log('🔄 Force reinitializing Textbox object');
|
|
27686
|
+
|
|
27687
|
+
// CRITICAL: Ensure textbox is marked as initialized
|
|
27688
|
+
this.initialized = true;
|
|
27689
|
+
|
|
27690
|
+
// Clear all caches and force dirty state
|
|
27691
|
+
this._clearCache();
|
|
27692
|
+
this.dirty = true;
|
|
27693
|
+
this.dynamicMinWidth = 0;
|
|
27694
|
+
|
|
27695
|
+
// Force isEditing false to ensure clean state
|
|
27696
|
+
this.isEditing = false;
|
|
27697
|
+
console.log(' → Set initialized=true, dirty=true, cleared caches');
|
|
27698
|
+
|
|
27699
|
+
// Re-initialize dimensions (this will handle justify properly)
|
|
27700
|
+
this.initDimensions();
|
|
27701
|
+
|
|
27702
|
+
// Double-check that justify was applied by checking space widths
|
|
27703
|
+
if (this.textAlign.includes('justify') && this.__charBounds) {
|
|
27704
|
+
setTimeout(() => {
|
|
27705
|
+
var _this$canvas6;
|
|
27706
|
+
// Verify justify was applied by checking if space widths vary
|
|
27707
|
+
let hasVariableSpaces = false;
|
|
27708
|
+
this.__charBounds.forEach((lineBounds, i) => {
|
|
27709
|
+
if (lineBounds && this._textLines && this._textLines[i]) {
|
|
27710
|
+
const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
|
|
27711
|
+
if (spaces.length > 1) {
|
|
27712
|
+
const firstSpaceWidth = spaces[0].width;
|
|
27713
|
+
hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
|
|
27714
|
+
}
|
|
27715
|
+
}
|
|
27716
|
+
});
|
|
27717
|
+
if (!hasVariableSpaces && this.__charBounds.length > 0) {
|
|
27718
|
+
console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
|
|
27719
|
+
if (this.enlargeSpaces) {
|
|
27720
|
+
this.enlargeSpaces();
|
|
27721
|
+
}
|
|
27722
|
+
} else {
|
|
27723
|
+
console.log(' ✅ Justify spaces properly expanded');
|
|
27724
|
+
}
|
|
27725
|
+
|
|
27726
|
+
// Ensure height is recalculated - use browser height if available
|
|
27727
|
+
if (this._usingBrowserWrapping && this._actualBrowserHeight) {
|
|
27728
|
+
this.height = this._actualBrowserHeight;
|
|
27729
|
+
console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
|
|
27730
|
+
} else {
|
|
27731
|
+
this.height = this.calcTextHeight();
|
|
27732
|
+
console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
|
|
27733
|
+
}
|
|
27734
|
+
(_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
|
|
27735
|
+
}, 10);
|
|
27736
|
+
}
|
|
27737
|
+
}
|
|
27738
|
+
|
|
25813
27739
|
/**
|
|
25814
27740
|
* Returns object representation of an instance
|
|
25815
27741
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|