@nasser-sw/fabric 7.0.1-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 +1853 -288
- 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 +1853 -288
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +1853 -288
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +1853 -288
- 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 +43 -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 +521 -67
- 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.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 +155 -9
- 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 +43 -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.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 +544 -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 +164 -12
- 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.mjs
CHANGED
|
@@ -354,7 +354,7 @@ class Cache {
|
|
|
354
354
|
}
|
|
355
355
|
const cache = new Cache();
|
|
356
356
|
|
|
357
|
-
var version = "7.0.
|
|
357
|
+
var version = "7.0.1-beta9";
|
|
358
358
|
|
|
359
359
|
// use this syntax so babel plugin see this import here
|
|
360
360
|
const VERSION = version;
|
|
@@ -17571,33 +17571,30 @@ class PatternBrush extends PencilBrush {
|
|
|
17571
17571
|
}
|
|
17572
17572
|
}
|
|
17573
17573
|
|
|
17574
|
-
// @TODO this code is terrible and Line should be a special case of polyline.
|
|
17575
|
-
|
|
17576
17574
|
const coordProps = ['x1', 'x2', 'y1', 'y2'];
|
|
17577
|
-
/**
|
|
17578
|
-
* A Class to draw a line
|
|
17579
|
-
* A bunch of methods will be added to Polyline to handle the line case
|
|
17580
|
-
* The line class is very strange to work with, is all special, it hardly aligns
|
|
17581
|
-
* to what a developer want everytime there is an angle
|
|
17582
|
-
* @deprecated
|
|
17583
|
-
*/
|
|
17584
17575
|
class Line extends FabricObject {
|
|
17585
|
-
/**
|
|
17586
|
-
* Constructor
|
|
17587
|
-
* @param {Array} [points] Array of points
|
|
17588
|
-
* @param {Object} [options] Options object
|
|
17589
|
-
* @return {Line} thisArg
|
|
17590
|
-
*/
|
|
17591
17576
|
constructor() {
|
|
17592
|
-
let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0,
|
|
17577
|
+
let [x1, y1, x2, y2] = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [0, 0, 100, 0];
|
|
17593
17578
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17594
17579
|
super();
|
|
17595
|
-
|
|
17580
|
+
_defineProperty(this, "hitStrokeWidth", 'auto');
|
|
17581
|
+
_defineProperty(this, "_updatingEndpoints", false);
|
|
17582
|
+
_defineProperty(this, "_useEndpointCoords", true);
|
|
17583
|
+
_defineProperty(this, "_exportingSVG", false);
|
|
17596
17584
|
this.setOptions(options);
|
|
17597
17585
|
this.x1 = x1;
|
|
17598
17586
|
this.x2 = x2;
|
|
17599
17587
|
this.y1 = y1;
|
|
17600
17588
|
this.y2 = y2;
|
|
17589
|
+
if (options.hitStrokeWidth !== undefined) {
|
|
17590
|
+
this.hitStrokeWidth = options.hitStrokeWidth;
|
|
17591
|
+
}
|
|
17592
|
+
this.hasBorders = false;
|
|
17593
|
+
this.hasControls = true;
|
|
17594
|
+
this.selectable = true;
|
|
17595
|
+
this.hoverCursor = 'move';
|
|
17596
|
+
this.perPixelTargetFind = false;
|
|
17597
|
+
this.strokeLineCap = 'butt';
|
|
17601
17598
|
this._setWidthHeight();
|
|
17602
17599
|
const {
|
|
17603
17600
|
left,
|
|
@@ -17605,129 +17602,384 @@ class Line extends FabricObject {
|
|
|
17605
17602
|
} = options;
|
|
17606
17603
|
typeof left === 'number' && this.set(LEFT, left);
|
|
17607
17604
|
typeof top === 'number' && this.set(TOP, top);
|
|
17605
|
+
this._setupLineControls();
|
|
17606
|
+
}
|
|
17607
|
+
_setupLineControls() {
|
|
17608
|
+
this.controls = {
|
|
17609
|
+
p1: new Control({
|
|
17610
|
+
x: 0,
|
|
17611
|
+
y: 0,
|
|
17612
|
+
cursorStyle: 'move',
|
|
17613
|
+
actionHandler: this._endpointActionHandler.bind(this),
|
|
17614
|
+
positionHandler: this._p1PositionHandler.bind(this),
|
|
17615
|
+
render: this._renderEndpointControl.bind(this),
|
|
17616
|
+
sizeX: 12,
|
|
17617
|
+
sizeY: 12
|
|
17618
|
+
}),
|
|
17619
|
+
p2: new Control({
|
|
17620
|
+
x: 0,
|
|
17621
|
+
y: 0,
|
|
17622
|
+
cursorStyle: 'move',
|
|
17623
|
+
actionHandler: this._endpointActionHandler.bind(this),
|
|
17624
|
+
positionHandler: this._p2PositionHandler.bind(this),
|
|
17625
|
+
render: this._renderEndpointControl.bind(this),
|
|
17626
|
+
sizeX: 12,
|
|
17627
|
+
sizeY: 12
|
|
17628
|
+
})
|
|
17629
|
+
};
|
|
17630
|
+
}
|
|
17631
|
+
_p1PositionHandler() {
|
|
17632
|
+
return new Point(this.x1, this.y1).transform(this.getViewportTransform());
|
|
17633
|
+
}
|
|
17634
|
+
_p2PositionHandler() {
|
|
17635
|
+
return new Point(this.x2, this.y2).transform(this.getViewportTransform());
|
|
17636
|
+
}
|
|
17637
|
+
_renderEndpointControl(ctx, left, top) {
|
|
17638
|
+
const size = 12;
|
|
17639
|
+
ctx.save();
|
|
17640
|
+
ctx.fillStyle = '#007bff';
|
|
17641
|
+
ctx.strokeStyle = '#ffffff';
|
|
17642
|
+
ctx.lineWidth = 2;
|
|
17643
|
+
ctx.beginPath();
|
|
17644
|
+
ctx.arc(left, top, size / 2, 0, 2 * Math.PI);
|
|
17645
|
+
ctx.fill();
|
|
17646
|
+
ctx.stroke();
|
|
17647
|
+
ctx.restore();
|
|
17648
|
+
}
|
|
17649
|
+
drawBorders(ctx) {
|
|
17650
|
+
let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17651
|
+
if (this._useEndpointCoords) {
|
|
17652
|
+
this._drawLineBorders(ctx, styleOverride);
|
|
17653
|
+
return this;
|
|
17654
|
+
}
|
|
17655
|
+
return super.drawBorders(ctx, styleOverride, {});
|
|
17656
|
+
}
|
|
17657
|
+
_drawLineBorders(ctx) {
|
|
17658
|
+
var _this$canvas;
|
|
17659
|
+
let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17660
|
+
const vpt = ((_this$canvas = this.canvas) === null || _this$canvas === void 0 ? void 0 : _this$canvas.viewportTransform) || [1, 0, 0, 1, 0, 0];
|
|
17661
|
+
ctx.save();
|
|
17662
|
+
ctx.setTransform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
|
|
17663
|
+
ctx.strokeStyle = styleOverride.borderColor || this.borderColor || 'rgba(100, 200, 200, 0.5)';
|
|
17664
|
+
ctx.lineWidth = (this.strokeWidth || 1) + 5;
|
|
17665
|
+
ctx.lineCap = this.strokeLineCap || 'butt';
|
|
17666
|
+
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
|
|
17667
|
+
ctx.beginPath();
|
|
17668
|
+
ctx.moveTo(this.x1, this.y1);
|
|
17669
|
+
ctx.lineTo(this.x2, this.y2);
|
|
17670
|
+
ctx.stroke();
|
|
17671
|
+
ctx.restore();
|
|
17672
|
+
}
|
|
17673
|
+
_renderControls(ctx) {
|
|
17674
|
+
let styleOverride = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
17675
|
+
ctx.save();
|
|
17676
|
+
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
|
|
17677
|
+
this.drawControls(ctx, styleOverride);
|
|
17678
|
+
ctx.restore();
|
|
17679
|
+
}
|
|
17680
|
+
getBoundingRect() {
|
|
17681
|
+
if (this._useEndpointCoords) {
|
|
17682
|
+
const {
|
|
17683
|
+
x1,
|
|
17684
|
+
y1,
|
|
17685
|
+
x2,
|
|
17686
|
+
y2
|
|
17687
|
+
} = this;
|
|
17688
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
|
|
17689
|
+
const padding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
|
|
17690
|
+
return {
|
|
17691
|
+
left: Math.min(x1, x2) - padding,
|
|
17692
|
+
top: Math.min(y1, y2) - padding,
|
|
17693
|
+
width: Math.abs(x2 - x1) + padding * 2 || padding * 2,
|
|
17694
|
+
height: Math.abs(y2 - y1) + padding * 2 || padding * 2
|
|
17695
|
+
};
|
|
17696
|
+
}
|
|
17697
|
+
return super.getBoundingRect();
|
|
17608
17698
|
}
|
|
17699
|
+
setCoords() {
|
|
17700
|
+
if (this._useEndpointCoords) {
|
|
17701
|
+
// Set width and height for hit detection and bounding box
|
|
17702
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
|
|
17703
|
+
const hitPadding = Math.max(effectiveStrokeWidth / 2 + 5, 10);
|
|
17704
|
+
this.width = Math.abs(this.x2 - this.x1) + hitPadding * 2;
|
|
17705
|
+
this.height = Math.abs(this.y2 - this.y1) + hitPadding * 2;
|
|
17609
17706
|
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17618
|
-
x2,
|
|
17619
|
-
y2
|
|
17620
|
-
} = this;
|
|
17621
|
-
this.width = Math.abs(x2 - x1);
|
|
17622
|
-
this.height = Math.abs(y2 - y1);
|
|
17623
|
-
const {
|
|
17624
|
-
left,
|
|
17625
|
-
top,
|
|
17626
|
-
width,
|
|
17627
|
-
height
|
|
17628
|
-
} = makeBoundingBoxFromPoints([{
|
|
17629
|
-
x: x1,
|
|
17630
|
-
y: y1
|
|
17631
|
-
}, {
|
|
17632
|
-
x: x2,
|
|
17633
|
-
y: y2
|
|
17634
|
-
}]);
|
|
17635
|
-
const position = new Point(left + width / 2, top + height / 2);
|
|
17636
|
-
this.setPositionByOrigin(position, CENTER, CENTER);
|
|
17707
|
+
// Only update left/top if they haven't been explicitly set (e.g., during loading)
|
|
17708
|
+
if (this.left === 0 && this.top === 0) {
|
|
17709
|
+
const center = this._findCenterFromElement();
|
|
17710
|
+
this.left = center.x;
|
|
17711
|
+
this.top = center.y;
|
|
17712
|
+
}
|
|
17713
|
+
}
|
|
17714
|
+
super.setCoords();
|
|
17637
17715
|
}
|
|
17716
|
+
getCoords() {
|
|
17717
|
+
if (this._useEndpointCoords) {
|
|
17718
|
+
const deltaX = this.x2 - this.x1;
|
|
17719
|
+
const deltaY = this.y2 - this.y1;
|
|
17720
|
+
const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
17721
|
+
if (length === 0) {
|
|
17722
|
+
return super.getCoords();
|
|
17723
|
+
}
|
|
17724
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth;
|
|
17725
|
+
const halfWidth = Math.max(effectiveStrokeWidth / 2 + 2, 5);
|
|
17638
17726
|
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17727
|
+
// Unit vector perpendicular to line
|
|
17728
|
+
const perpX = -deltaY / length;
|
|
17729
|
+
const perpY = deltaX / length;
|
|
17730
|
+
|
|
17731
|
+
// Four corners of oriented rectangle
|
|
17732
|
+
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)];
|
|
17733
|
+
}
|
|
17734
|
+
return super.getCoords();
|
|
17735
|
+
}
|
|
17736
|
+
containsPoint(point) {
|
|
17737
|
+
if (this._useEndpointCoords) {
|
|
17738
|
+
var _this$canvas2;
|
|
17739
|
+
if (((_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 ? void 0 : _this$canvas2.getActiveObject()) === this) {
|
|
17740
|
+
return super.containsPoint(point);
|
|
17741
|
+
}
|
|
17742
|
+
const distance = this._distanceToLineSegment(point.x, point.y);
|
|
17743
|
+
const effectiveStrokeWidth = this.hitStrokeWidth === 'auto' ? this.strokeWidth : this.hitStrokeWidth || 1;
|
|
17744
|
+
const tolerance = Math.max(effectiveStrokeWidth / 2 + 2, 5);
|
|
17745
|
+
return distance <= tolerance;
|
|
17746
|
+
}
|
|
17747
|
+
return super.containsPoint(point);
|
|
17748
|
+
}
|
|
17749
|
+
_distanceToLineSegment(px, py) {
|
|
17750
|
+
const x1 = this.x1,
|
|
17751
|
+
y1 = this.y1,
|
|
17752
|
+
x2 = this.x2,
|
|
17753
|
+
y2 = this.y2;
|
|
17754
|
+
const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
|
|
17755
|
+
if (pd2 === 0) {
|
|
17756
|
+
return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
|
|
17757
|
+
}
|
|
17758
|
+
const u = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / pd2;
|
|
17759
|
+
let closestX, closestY;
|
|
17760
|
+
if (u < 0) {
|
|
17761
|
+
closestX = x1;
|
|
17762
|
+
closestY = y1;
|
|
17763
|
+
} else if (u > 1) {
|
|
17764
|
+
closestX = x2;
|
|
17765
|
+
closestY = y2;
|
|
17766
|
+
} else {
|
|
17767
|
+
closestX = x1 + u * (x2 - x1);
|
|
17768
|
+
closestY = y1 + u * (y2 - y1);
|
|
17769
|
+
}
|
|
17770
|
+
return Math.sqrt((px - closestX) * (px - closestX) + (py - closestY) * (py - closestY));
|
|
17771
|
+
}
|
|
17772
|
+
_endpointActionHandler(eventData, transformData, x, y) {
|
|
17773
|
+
var _this$canvas4;
|
|
17774
|
+
const controlKey = transformData.corner;
|
|
17775
|
+
const pointer = new Point(x, y);
|
|
17776
|
+
let newX = pointer.x;
|
|
17777
|
+
let newY = pointer.y;
|
|
17778
|
+
if (eventData.shiftKey) {
|
|
17779
|
+
const otherControl = controlKey === 'p1' ? 'p2' : 'p1';
|
|
17780
|
+
const otherX = this[otherControl === 'p1' ? 'x1' : 'x2'];
|
|
17781
|
+
const otherY = this[otherControl === 'p1' ? 'y1' : 'y2'];
|
|
17782
|
+
const snapped = this._snapToAngle(otherX, otherY, newX, newY);
|
|
17783
|
+
newX = snapped.x;
|
|
17784
|
+
newY = snapped.y;
|
|
17785
|
+
}
|
|
17786
|
+
if (this._useEndpointCoords) {
|
|
17787
|
+
var _this$canvas3;
|
|
17788
|
+
if (controlKey === 'p1') {
|
|
17789
|
+
this.x1 = newX;
|
|
17790
|
+
this.y1 = newY;
|
|
17791
|
+
} else if (controlKey === 'p2') {
|
|
17792
|
+
this.x2 = newX;
|
|
17793
|
+
this.y2 = newY;
|
|
17794
|
+
}
|
|
17795
|
+
|
|
17796
|
+
// Update gradient coordinates if stroke is a gradient (but not during SVG export)
|
|
17797
|
+
if (this.stroke instanceof Gradient && !this._exportingSVG) {
|
|
17798
|
+
this.stroke.coords.x1 = this.x1;
|
|
17799
|
+
this.stroke.coords.y1 = this.y1;
|
|
17800
|
+
this.stroke.coords.x2 = this.x2;
|
|
17801
|
+
this.stroke.coords.y2 = this.y2;
|
|
17802
|
+
}
|
|
17803
|
+
this.dirty = true;
|
|
17804
|
+
this.setCoords();
|
|
17805
|
+
(_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
|
|
17806
|
+
return true;
|
|
17807
|
+
}
|
|
17808
|
+
|
|
17809
|
+
// Fallback for old system
|
|
17810
|
+
this._updatingEndpoints = true;
|
|
17811
|
+
if (controlKey === 'p1') {
|
|
17812
|
+
this.x1 = newX;
|
|
17813
|
+
this.y1 = newY;
|
|
17814
|
+
} else if (controlKey === 'p2') {
|
|
17815
|
+
this.x2 = newX;
|
|
17816
|
+
this.y2 = newY;
|
|
17817
|
+
}
|
|
17818
|
+
this._setWidthHeight();
|
|
17819
|
+
this.dirty = true;
|
|
17820
|
+
this._updatingEndpoints = false;
|
|
17821
|
+
(_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
|
|
17822
|
+
this.fire('modified', {
|
|
17823
|
+
transform: transformData,
|
|
17824
|
+
target: this,
|
|
17825
|
+
e: eventData
|
|
17826
|
+
});
|
|
17827
|
+
return true;
|
|
17828
|
+
}
|
|
17829
|
+
_snapToAngle(fromX, fromY, toX, toY) {
|
|
17830
|
+
const deltaX = toX - fromX;
|
|
17831
|
+
const deltaY = toY - fromY;
|
|
17832
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
17833
|
+
if (distance === 0) return {
|
|
17834
|
+
x: toX,
|
|
17835
|
+
y: toY
|
|
17836
|
+
};
|
|
17837
|
+
let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
|
|
17838
|
+
const snapIncrement = 15;
|
|
17839
|
+
const snappedAngle = Math.round(angle / snapIncrement) * snapIncrement;
|
|
17840
|
+
const snappedRadians = snappedAngle * (Math.PI / 180);
|
|
17841
|
+
return {
|
|
17842
|
+
x: fromX + Math.cos(snappedRadians) * distance,
|
|
17843
|
+
y: fromY + Math.sin(snappedRadians) * distance
|
|
17844
|
+
};
|
|
17845
|
+
}
|
|
17846
|
+
_setWidthHeight() {
|
|
17847
|
+
let skipReposition = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
|
17848
|
+
this.width = Math.abs(this.x2 - this.x1) || 1;
|
|
17849
|
+
this.height = Math.abs(this.y2 - this.y1) || 1;
|
|
17850
|
+
if (!skipReposition && !this._updatingEndpoints) {
|
|
17851
|
+
const {
|
|
17852
|
+
left,
|
|
17853
|
+
top,
|
|
17854
|
+
width,
|
|
17855
|
+
height
|
|
17856
|
+
} = makeBoundingBoxFromPoints([{
|
|
17857
|
+
x: this.x1,
|
|
17858
|
+
y: this.y1
|
|
17859
|
+
}, {
|
|
17860
|
+
x: this.x2,
|
|
17861
|
+
y: this.y2
|
|
17862
|
+
}]);
|
|
17863
|
+
this.setPositionByOrigin(new Point(left + width / 2, top + height / 2), CENTER, CENTER);
|
|
17864
|
+
}
|
|
17865
|
+
}
|
|
17644
17866
|
_set(key, value) {
|
|
17867
|
+
const oldLeft = this.left;
|
|
17868
|
+
const oldTop = this.top;
|
|
17645
17869
|
super._set(key, value);
|
|
17646
17870
|
if (coordProps.includes(key)) {
|
|
17647
|
-
// this doesn't make sense very much, since setting x1 when top or left
|
|
17648
|
-
// are already set, is just going to show a strange result since the
|
|
17649
|
-
// line will move way more than the developer expect.
|
|
17650
|
-
// in fabric5 it worked only when the line didn't have extra transformations,
|
|
17651
|
-
// in fabric6 too. With extra transform they behave bad in different ways.
|
|
17652
|
-
// This needs probably a good rework or a tutorial if you have to create a dynamic line
|
|
17653
17871
|
this._setWidthHeight();
|
|
17872
|
+
this.dirty = true;
|
|
17873
|
+
|
|
17874
|
+
// Update gradient coordinates if stroke is a gradient (but not during SVG export)
|
|
17875
|
+
if (this.stroke instanceof Gradient && !this._exportingSVG) {
|
|
17876
|
+
this.stroke.coords.x1 = this.x1;
|
|
17877
|
+
this.stroke.coords.y1 = this.y1;
|
|
17878
|
+
this.stroke.coords.x2 = this.x2;
|
|
17879
|
+
this.stroke.coords.y2 = this.y2;
|
|
17880
|
+
}
|
|
17881
|
+
}
|
|
17882
|
+
if ((key === 'left' || key === 'top') && this.canvas && !this._updatingEndpoints) {
|
|
17883
|
+
const deltaX = this.left - oldLeft;
|
|
17884
|
+
const deltaY = this.top - oldTop;
|
|
17885
|
+
if (deltaX !== 0 || deltaY !== 0) {
|
|
17886
|
+
this._updatingEndpoints = true;
|
|
17887
|
+
this.x1 += deltaX;
|
|
17888
|
+
this.y1 += deltaY;
|
|
17889
|
+
this.x2 += deltaX;
|
|
17890
|
+
this.y2 += deltaY;
|
|
17891
|
+
|
|
17892
|
+
// Update gradient coordinates if stroke is a gradient
|
|
17893
|
+
if (this.stroke instanceof Gradient) {
|
|
17894
|
+
this.stroke.coords.x1 = this.x1;
|
|
17895
|
+
this.stroke.coords.y1 = this.y1;
|
|
17896
|
+
this.stroke.coords.x2 = this.x2;
|
|
17897
|
+
this.stroke.coords.y2 = this.y2;
|
|
17898
|
+
}
|
|
17899
|
+
this._updatingEndpoints = false;
|
|
17900
|
+
}
|
|
17654
17901
|
}
|
|
17655
17902
|
return this;
|
|
17656
17903
|
}
|
|
17657
|
-
|
|
17658
|
-
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
|
|
17904
|
+
render(ctx) {
|
|
17905
|
+
if (this._useEndpointCoords) {
|
|
17906
|
+
this._renderDirectly(ctx);
|
|
17907
|
+
return;
|
|
17908
|
+
}
|
|
17909
|
+
super.render(ctx);
|
|
17910
|
+
}
|
|
17911
|
+
_renderDirectly(ctx) {
|
|
17912
|
+
if (!this.visible) return;
|
|
17913
|
+
ctx.save();
|
|
17914
|
+
ctx.globalAlpha = this.opacity;
|
|
17915
|
+
ctx.lineWidth = this.strokeWidth;
|
|
17916
|
+
ctx.lineCap = this.strokeLineCap || 'butt';
|
|
17917
|
+
ctx.beginPath();
|
|
17918
|
+
ctx.moveTo(this.x1, this.y1);
|
|
17919
|
+
ctx.lineTo(this.x2, this.y2);
|
|
17920
|
+
const origStrokeStyle = ctx.strokeStyle;
|
|
17921
|
+
if (isFiller(this.stroke)) {
|
|
17922
|
+
ctx.strokeStyle = this.stroke.toLive(ctx);
|
|
17923
|
+
} else {
|
|
17924
|
+
var _this$stroke;
|
|
17925
|
+
ctx.strokeStyle = ((_this$stroke = this.stroke) === null || _this$stroke === void 0 ? void 0 : _this$stroke.toString()) || '#000';
|
|
17926
|
+
}
|
|
17927
|
+
ctx.stroke();
|
|
17928
|
+
ctx.strokeStyle = origStrokeStyle;
|
|
17929
|
+
ctx.restore();
|
|
17930
|
+
}
|
|
17662
17931
|
_render(ctx) {
|
|
17932
|
+
if (this._useEndpointCoords) return;
|
|
17663
17933
|
ctx.beginPath();
|
|
17664
17934
|
const p = this.calcLinePoints();
|
|
17665
17935
|
ctx.moveTo(p.x1, p.y1);
|
|
17666
17936
|
ctx.lineTo(p.x2, p.y2);
|
|
17667
17937
|
ctx.lineWidth = this.strokeWidth;
|
|
17668
|
-
|
|
17669
|
-
// TODO: test this
|
|
17670
|
-
// make sure setting "fill" changes color of a line
|
|
17671
|
-
// (by copying fillStyle to strokeStyle, since line is stroked, not filled)
|
|
17672
17938
|
const origStrokeStyle = ctx.strokeStyle;
|
|
17673
17939
|
if (isFiller(this.stroke)) {
|
|
17674
17940
|
ctx.strokeStyle = this.stroke.toLive(ctx);
|
|
17675
|
-
} else {
|
|
17676
|
-
var _this$stroke;
|
|
17677
|
-
ctx.strokeStyle = (_this$stroke = this.stroke) !== null && _this$stroke !== void 0 ? _this$stroke : ctx.fillStyle;
|
|
17678
17941
|
}
|
|
17679
17942
|
this.stroke && this._renderStroke(ctx);
|
|
17680
17943
|
ctx.strokeStyle = origStrokeStyle;
|
|
17681
17944
|
}
|
|
17682
|
-
|
|
17683
|
-
/**
|
|
17684
|
-
* This function is an helper for svg import. it returns the center of the object in the svg
|
|
17685
|
-
* untransformed coordinates
|
|
17686
|
-
* @private
|
|
17687
|
-
* @return {Point} center point from element coordinates
|
|
17688
|
-
*/
|
|
17689
17945
|
_findCenterFromElement() {
|
|
17690
17946
|
return new Point((this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
|
|
17691
17947
|
}
|
|
17692
|
-
|
|
17693
|
-
/**
|
|
17694
|
-
* Returns object representation of an instance
|
|
17695
|
-
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
17696
|
-
* @return {Object} object representation of an instance
|
|
17697
|
-
*/
|
|
17698
17948
|
toObject() {
|
|
17699
17949
|
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
17950
|
+
if (this._useEndpointCoords) {
|
|
17951
|
+
return {
|
|
17952
|
+
...super.toObject(propertiesToInclude),
|
|
17953
|
+
x1: this.x1,
|
|
17954
|
+
y1: this.y1,
|
|
17955
|
+
x2: this.x2,
|
|
17956
|
+
y2: this.y2
|
|
17957
|
+
};
|
|
17958
|
+
}
|
|
17700
17959
|
return {
|
|
17701
17960
|
...super.toObject(propertiesToInclude),
|
|
17702
17961
|
...this.calcLinePoints()
|
|
17703
17962
|
};
|
|
17704
17963
|
}
|
|
17705
|
-
|
|
17706
|
-
/*
|
|
17707
|
-
* Calculate object dimensions from its properties
|
|
17708
|
-
* @private
|
|
17709
|
-
*/
|
|
17710
17964
|
_getNonTransformedDimensions() {
|
|
17711
17965
|
const dim = super._getNonTransformedDimensions();
|
|
17712
|
-
if (this.strokeLineCap === '
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
}
|
|
17716
|
-
if (this.height === 0) {
|
|
17717
|
-
dim.x -= this.strokeWidth;
|
|
17718
|
-
}
|
|
17966
|
+
if (this.strokeLineCap === 'round') {
|
|
17967
|
+
dim.x += this.strokeWidth;
|
|
17968
|
+
dim.y += this.strokeWidth;
|
|
17719
17969
|
}
|
|
17720
17970
|
return dim;
|
|
17721
17971
|
}
|
|
17722
|
-
|
|
17723
|
-
/**
|
|
17724
|
-
* Recalculates line points given width and height
|
|
17725
|
-
* Those points are simply placed around the center,
|
|
17726
|
-
* This is not useful outside internal render functions and svg output
|
|
17727
|
-
* Is not meant to be for the developer.
|
|
17728
|
-
* @private
|
|
17729
|
-
*/
|
|
17730
17972
|
calcLinePoints() {
|
|
17973
|
+
if (this._updatingEndpoints) {
|
|
17974
|
+
const centerX = (this.x1 + this.x2) / 2;
|
|
17975
|
+
const centerY = (this.y1 + this.y2) / 2;
|
|
17976
|
+
return {
|
|
17977
|
+
x1: this.x1 - centerX,
|
|
17978
|
+
y1: this.y1 - centerY,
|
|
17979
|
+
x2: this.x2 - centerX,
|
|
17980
|
+
y2: this.y2 - centerY
|
|
17981
|
+
};
|
|
17982
|
+
}
|
|
17731
17983
|
const {
|
|
17732
17984
|
x1: _x1,
|
|
17733
17985
|
x2: _x2,
|
|
@@ -17736,48 +17988,64 @@ class Line extends FabricObject {
|
|
|
17736
17988
|
width,
|
|
17737
17989
|
height
|
|
17738
17990
|
} = this;
|
|
17739
|
-
const xMult = _x1 <= _x2 ? -1 : 1
|
|
17740
|
-
|
|
17741
|
-
x1 = xMult * width / 2,
|
|
17742
|
-
y1 = yMult * height / 2,
|
|
17743
|
-
x2 = xMult * -width / 2,
|
|
17744
|
-
y2 = yMult * -height / 2;
|
|
17991
|
+
const xMult = _x1 <= _x2 ? -1 : 1;
|
|
17992
|
+
const yMult = _y1 <= _y2 ? -1 : 1;
|
|
17745
17993
|
return {
|
|
17746
|
-
x1,
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
y2
|
|
17994
|
+
x1: xMult * width / 2,
|
|
17995
|
+
y1: yMult * height / 2,
|
|
17996
|
+
x2: xMult * -width / 2,
|
|
17997
|
+
y2: yMult * -height / 2
|
|
17750
17998
|
};
|
|
17751
17999
|
}
|
|
17752
|
-
|
|
17753
|
-
/* _FROM_SVG_START_ */
|
|
17754
|
-
|
|
17755
|
-
/**
|
|
17756
|
-
* Returns svg representation of an instance
|
|
17757
|
-
* @return {Array} an array of strings with the specific svg representation
|
|
17758
|
-
* of the instance
|
|
17759
|
-
*/
|
|
17760
18000
|
_toSVG() {
|
|
17761
|
-
|
|
17762
|
-
|
|
17763
|
-
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
18001
|
+
if (this._useEndpointCoords) {
|
|
18002
|
+
// Use absolute coordinates to bypass all Fabric.js transforms
|
|
18003
|
+
// Handle gradients manually for proper SVG export
|
|
18004
|
+
let strokeAttr = '';
|
|
18005
|
+
if (this.stroke instanceof Gradient) {
|
|
18006
|
+
// Let Fabric.js handle gradient definition, but we'll use the reference
|
|
18007
|
+
strokeAttr = `stroke="url(#${this.stroke.id})"`;
|
|
18008
|
+
} else {
|
|
18009
|
+
strokeAttr = `stroke="${this.stroke || 'none'}"`;
|
|
18010
|
+
}
|
|
18011
|
+
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`];
|
|
18012
|
+
} else {
|
|
18013
|
+
// Use standard calcLinePoints for legacy mode
|
|
18014
|
+
const {
|
|
18015
|
+
x1,
|
|
18016
|
+
x2,
|
|
18017
|
+
y1,
|
|
18018
|
+
y2
|
|
18019
|
+
} = this.calcLinePoints();
|
|
18020
|
+
return ['<line ', 'COMMON_PARTS', `x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`];
|
|
18021
|
+
}
|
|
17768
18022
|
}
|
|
18023
|
+
toSVG(reviver) {
|
|
18024
|
+
if (this._useEndpointCoords) {
|
|
18025
|
+
// For endpoint coords, we need to bypass transforms but still allow gradients
|
|
18026
|
+
// Let's temporarily disable transforms during SVG generation
|
|
18027
|
+
const originalLeft = this.left;
|
|
18028
|
+
const originalTop = this.top;
|
|
17769
18029
|
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
*/
|
|
18030
|
+
// Set position to center of line for gradient calculation
|
|
18031
|
+
this.left = (this.x1 + this.x2) / 2;
|
|
18032
|
+
this.top = (this.y1 + this.y2) / 2;
|
|
17774
18033
|
|
|
17775
|
-
|
|
17776
|
-
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
18034
|
+
// Get the SVG with standard system (for gradient handling)
|
|
18035
|
+
const standardSVG = super.toSVG(reviver);
|
|
18036
|
+
|
|
18037
|
+
// Restore original position
|
|
18038
|
+
this.left = originalLeft;
|
|
18039
|
+
this.top = originalTop;
|
|
18040
|
+
|
|
18041
|
+
// Extract gradient definition and clean up the line element
|
|
18042
|
+
// Remove the transform wrapper and update coordinates
|
|
18043
|
+
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}"`);
|
|
18044
|
+
return cleanSVG;
|
|
18045
|
+
}
|
|
18046
|
+
// Use default behavior for legacy mode
|
|
18047
|
+
return super.toSVG(reviver);
|
|
18048
|
+
}
|
|
17781
18049
|
static async fromElement(element, options, cssRules) {
|
|
17782
18050
|
const {
|
|
17783
18051
|
x1 = 0,
|
|
@@ -17788,14 +18056,6 @@ class Line extends FabricObject {
|
|
|
17788
18056
|
} = parseAttributes(element, this.ATTRIBUTE_NAMES, cssRules);
|
|
17789
18057
|
return new this([x1, y1, x2, y2], parsedAttributes);
|
|
17790
18058
|
}
|
|
17791
|
-
|
|
17792
|
-
/* _FROM_SVG_END_ */
|
|
17793
|
-
|
|
17794
|
-
/**
|
|
17795
|
-
* Returns Line instance from an object representation
|
|
17796
|
-
* @param {Object} object Object to create an instance from
|
|
17797
|
-
* @returns {Promise<Line>}
|
|
17798
|
-
*/
|
|
17799
18059
|
static fromObject(_ref) {
|
|
17800
18060
|
let {
|
|
17801
18061
|
x1,
|
|
@@ -17812,32 +18072,195 @@ class Line extends FabricObject {
|
|
|
17812
18072
|
});
|
|
17813
18073
|
}
|
|
17814
18074
|
}
|
|
18075
|
+
_defineProperty(Line, "type", 'Line');
|
|
18076
|
+
_defineProperty(Line, "cacheProperties", [...cacheProperties, ...coordProps]);
|
|
18077
|
+
_defineProperty(Line, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat(coordProps));
|
|
18078
|
+
classRegistry.setClass(Line);
|
|
18079
|
+
classRegistry.setSVGClass(Line);
|
|
18080
|
+
|
|
18081
|
+
/**
|
|
18082
|
+
* Calculate the distance between two points
|
|
18083
|
+
*/
|
|
18084
|
+
function pointDistance(p1, p2) {
|
|
18085
|
+
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
|
18086
|
+
}
|
|
18087
|
+
|
|
18088
|
+
/**
|
|
18089
|
+
* Normalize a vector
|
|
18090
|
+
*/
|
|
18091
|
+
function normalizeVector(vector) {
|
|
18092
|
+
const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
|
|
18093
|
+
if (length === 0) return {
|
|
18094
|
+
x: 0,
|
|
18095
|
+
y: 0
|
|
18096
|
+
};
|
|
18097
|
+
return {
|
|
18098
|
+
x: vector.x / length,
|
|
18099
|
+
y: vector.y / length
|
|
18100
|
+
};
|
|
18101
|
+
}
|
|
18102
|
+
|
|
17815
18103
|
/**
|
|
17816
|
-
*
|
|
17817
|
-
* @type number
|
|
18104
|
+
* Get the maximum allowed radius for a corner based on adjacent edge lengths
|
|
17818
18105
|
*/
|
|
18106
|
+
function getMaxRadius(prevPoint, currentPoint, nextPoint) {
|
|
18107
|
+
const dist1 = pointDistance(prevPoint, currentPoint);
|
|
18108
|
+
const dist2 = pointDistance(currentPoint, nextPoint);
|
|
18109
|
+
return Math.min(dist1, dist2) / 2;
|
|
18110
|
+
}
|
|
18111
|
+
|
|
17819
18112
|
/**
|
|
17820
|
-
*
|
|
17821
|
-
* @type number
|
|
18113
|
+
* Calculate rounded corner data for a single corner
|
|
17822
18114
|
*/
|
|
18115
|
+
function calculateRoundedCorner(prevPoint, currentPoint, nextPoint, radius) {
|
|
18116
|
+
// Calculate edge vectors
|
|
18117
|
+
const edge1 = {
|
|
18118
|
+
x: currentPoint.x - prevPoint.x,
|
|
18119
|
+
y: currentPoint.y - prevPoint.y
|
|
18120
|
+
};
|
|
18121
|
+
const edge2 = {
|
|
18122
|
+
x: nextPoint.x - currentPoint.x,
|
|
18123
|
+
y: nextPoint.y - currentPoint.y
|
|
18124
|
+
};
|
|
18125
|
+
|
|
18126
|
+
// Normalize edge vectors
|
|
18127
|
+
const norm1 = normalizeVector(edge1);
|
|
18128
|
+
const norm2 = normalizeVector(edge2);
|
|
18129
|
+
|
|
18130
|
+
// Calculate the maximum allowed radius
|
|
18131
|
+
const maxRadius = getMaxRadius(prevPoint, currentPoint, nextPoint);
|
|
18132
|
+
const actualRadius = Math.min(radius, maxRadius);
|
|
18133
|
+
|
|
18134
|
+
// Calculate start and end points of the rounded corner
|
|
18135
|
+
const startPoint = {
|
|
18136
|
+
x: currentPoint.x - norm1.x * actualRadius,
|
|
18137
|
+
y: currentPoint.y - norm1.y * actualRadius
|
|
18138
|
+
};
|
|
18139
|
+
const endPoint = {
|
|
18140
|
+
x: currentPoint.x + norm2.x * actualRadius,
|
|
18141
|
+
y: currentPoint.y + norm2.y * actualRadius
|
|
18142
|
+
};
|
|
18143
|
+
|
|
18144
|
+
// Calculate control points for bezier curve
|
|
18145
|
+
// Using the magic number kRect for optimal circular approximation
|
|
18146
|
+
const controlOffset = actualRadius * kRect;
|
|
18147
|
+
const cp1 = {
|
|
18148
|
+
x: startPoint.x + norm1.x * controlOffset,
|
|
18149
|
+
y: startPoint.y + norm1.y * controlOffset
|
|
18150
|
+
};
|
|
18151
|
+
const cp2 = {
|
|
18152
|
+
x: endPoint.x - norm2.x * controlOffset,
|
|
18153
|
+
y: endPoint.y - norm2.y * controlOffset
|
|
18154
|
+
};
|
|
18155
|
+
return {
|
|
18156
|
+
corner: currentPoint,
|
|
18157
|
+
start: startPoint,
|
|
18158
|
+
end: endPoint,
|
|
18159
|
+
cp1,
|
|
18160
|
+
cp2,
|
|
18161
|
+
actualRadius
|
|
18162
|
+
};
|
|
18163
|
+
}
|
|
18164
|
+
|
|
17823
18165
|
/**
|
|
17824
|
-
*
|
|
17825
|
-
|
|
18166
|
+
* Apply corner radius to a polygon defined by points
|
|
18167
|
+
*/
|
|
18168
|
+
function applyCornerRadiusToPolygon(points, radius) {
|
|
18169
|
+
let radiusAsPercentage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
18170
|
+
if (points.length < 3) {
|
|
18171
|
+
throw new Error('Polygon must have at least 3 points');
|
|
18172
|
+
}
|
|
18173
|
+
|
|
18174
|
+
// Calculate bounding box if radius is percentage-based
|
|
18175
|
+
let actualRadius = radius;
|
|
18176
|
+
if (radiusAsPercentage) {
|
|
18177
|
+
const minX = Math.min(...points.map(p => p.x));
|
|
18178
|
+
const maxX = Math.max(...points.map(p => p.x));
|
|
18179
|
+
const minY = Math.min(...points.map(p => p.y));
|
|
18180
|
+
const maxY = Math.max(...points.map(p => p.y));
|
|
18181
|
+
const width = maxX - minX;
|
|
18182
|
+
const height = maxY - minY;
|
|
18183
|
+
const minDimension = Math.min(width, height);
|
|
18184
|
+
actualRadius = radius / 100 * minDimension;
|
|
18185
|
+
}
|
|
18186
|
+
const roundedCorners = [];
|
|
18187
|
+
for (let i = 0; i < points.length; i++) {
|
|
18188
|
+
const prevIndex = (i - 1 + points.length) % points.length;
|
|
18189
|
+
const nextIndex = (i + 1) % points.length;
|
|
18190
|
+
const prevPoint = points[prevIndex];
|
|
18191
|
+
const currentPoint = points[i];
|
|
18192
|
+
const nextPoint = points[nextIndex];
|
|
18193
|
+
const roundedCorner = calculateRoundedCorner(prevPoint, currentPoint, nextPoint, actualRadius);
|
|
18194
|
+
roundedCorners.push(roundedCorner);
|
|
18195
|
+
}
|
|
18196
|
+
return roundedCorners;
|
|
18197
|
+
}
|
|
18198
|
+
|
|
18199
|
+
/**
|
|
18200
|
+
* Render a rounded polygon to a canvas context
|
|
17826
18201
|
*/
|
|
18202
|
+
function renderRoundedPolygon(ctx, roundedCorners) {
|
|
18203
|
+
let closed = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
18204
|
+
if (roundedCorners.length === 0) return;
|
|
18205
|
+
ctx.beginPath();
|
|
18206
|
+
|
|
18207
|
+
// Start at the first corner's start point
|
|
18208
|
+
const firstCorner = roundedCorners[0];
|
|
18209
|
+
ctx.moveTo(firstCorner.start.x, firstCorner.start.y);
|
|
18210
|
+
for (let i = 0; i < roundedCorners.length; i++) {
|
|
18211
|
+
const corner = roundedCorners[i];
|
|
18212
|
+
const nextIndex = (i + 1) % roundedCorners.length;
|
|
18213
|
+
const nextCorner = roundedCorners[nextIndex];
|
|
18214
|
+
|
|
18215
|
+
// Draw the rounded corner using bezier curve
|
|
18216
|
+
ctx.bezierCurveTo(corner.cp1.x, corner.cp1.y, corner.cp2.x, corner.cp2.y, corner.end.x, corner.end.y);
|
|
18217
|
+
|
|
18218
|
+
// Draw line to next corner's start point (if not the last segment in open path)
|
|
18219
|
+
if (i < roundedCorners.length - 1 || closed) {
|
|
18220
|
+
ctx.lineTo(nextCorner.start.x, nextCorner.start.y);
|
|
18221
|
+
}
|
|
18222
|
+
}
|
|
18223
|
+
if (closed) {
|
|
18224
|
+
ctx.closePath();
|
|
18225
|
+
}
|
|
18226
|
+
}
|
|
18227
|
+
|
|
17827
18228
|
/**
|
|
17828
|
-
*
|
|
17829
|
-
* @type number
|
|
18229
|
+
* Generate SVG path data for a rounded polygon
|
|
17830
18230
|
*/
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
18231
|
+
function generateRoundedPolygonPath(roundedCorners) {
|
|
18232
|
+
let closed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
|
|
18233
|
+
if (roundedCorners.length === 0) return '';
|
|
18234
|
+
const pathData = [];
|
|
18235
|
+
const firstCorner = roundedCorners[0];
|
|
18236
|
+
|
|
18237
|
+
// Move to first corner's start point
|
|
18238
|
+
pathData.push(`M ${firstCorner.start.x} ${firstCorner.start.y}`);
|
|
18239
|
+
for (let i = 0; i < roundedCorners.length; i++) {
|
|
18240
|
+
const corner = roundedCorners[i];
|
|
18241
|
+
const nextIndex = (i + 1) % roundedCorners.length;
|
|
18242
|
+
const nextCorner = roundedCorners[nextIndex];
|
|
18243
|
+
|
|
18244
|
+
// Add bezier curve for the rounded corner
|
|
18245
|
+
pathData.push(`C ${corner.cp1.x} ${corner.cp1.y} ${corner.cp2.x} ${corner.cp2.y} ${corner.end.x} ${corner.end.y}`);
|
|
18246
|
+
|
|
18247
|
+
// Add line to next corner's start point (if not the last segment in open path)
|
|
18248
|
+
if (i < roundedCorners.length - 1 || closed) {
|
|
18249
|
+
pathData.push(`L ${nextCorner.start.x} ${nextCorner.start.y}`);
|
|
18250
|
+
}
|
|
18251
|
+
}
|
|
18252
|
+
if (closed) {
|
|
18253
|
+
pathData.push('Z');
|
|
18254
|
+
}
|
|
18255
|
+
return pathData.join(' ');
|
|
18256
|
+
}
|
|
17836
18257
|
|
|
17837
18258
|
const triangleDefaultValues = {
|
|
17838
18259
|
width: 100,
|
|
17839
|
-
height: 100
|
|
18260
|
+
height: 100,
|
|
18261
|
+
cornerRadius: 0
|
|
17840
18262
|
};
|
|
18263
|
+
const TRIANGLE_PROPS = ['cornerRadius'];
|
|
17841
18264
|
class Triangle extends FabricObject {
|
|
17842
18265
|
static getDefaults() {
|
|
17843
18266
|
return {
|
|
@@ -17856,34 +18279,90 @@ class Triangle extends FabricObject {
|
|
|
17856
18279
|
this.setOptions(options);
|
|
17857
18280
|
}
|
|
17858
18281
|
|
|
18282
|
+
/**
|
|
18283
|
+
* Get triangle points as an array of XY coordinates
|
|
18284
|
+
* @private
|
|
18285
|
+
*/
|
|
18286
|
+
_getTrianglePoints() {
|
|
18287
|
+
const widthBy2 = this.width / 2;
|
|
18288
|
+
const heightBy2 = this.height / 2;
|
|
18289
|
+
return [{
|
|
18290
|
+
x: -widthBy2,
|
|
18291
|
+
y: heightBy2
|
|
18292
|
+
},
|
|
18293
|
+
// bottom left
|
|
18294
|
+
{
|
|
18295
|
+
x: 0,
|
|
18296
|
+
y: -heightBy2
|
|
18297
|
+
},
|
|
18298
|
+
// top center
|
|
18299
|
+
{
|
|
18300
|
+
x: widthBy2,
|
|
18301
|
+
y: heightBy2
|
|
18302
|
+
} // bottom right
|
|
18303
|
+
];
|
|
18304
|
+
}
|
|
18305
|
+
|
|
17859
18306
|
/**
|
|
17860
18307
|
* @private
|
|
17861
18308
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
17862
18309
|
*/
|
|
17863
18310
|
_render(ctx) {
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
18311
|
+
if (this.cornerRadius > 0) {
|
|
18312
|
+
// Render rounded triangle
|
|
18313
|
+
const points = this._getTrianglePoints();
|
|
18314
|
+
const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
|
|
18315
|
+
renderRoundedPolygon(ctx, roundedCorners, true);
|
|
18316
|
+
} else {
|
|
18317
|
+
// Render sharp triangle (original implementation)
|
|
18318
|
+
const widthBy2 = this.width / 2;
|
|
18319
|
+
const heightBy2 = this.height / 2;
|
|
18320
|
+
ctx.beginPath();
|
|
18321
|
+
ctx.moveTo(-widthBy2, heightBy2);
|
|
18322
|
+
ctx.lineTo(0, -heightBy2);
|
|
18323
|
+
ctx.lineTo(widthBy2, heightBy2);
|
|
18324
|
+
ctx.closePath();
|
|
18325
|
+
}
|
|
17871
18326
|
this._renderPaintInOrder(ctx);
|
|
17872
18327
|
}
|
|
17873
18328
|
|
|
18329
|
+
/**
|
|
18330
|
+
* Returns object representation of an instance
|
|
18331
|
+
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
18332
|
+
* @return {Object} object representation of an instance
|
|
18333
|
+
*/
|
|
18334
|
+
toObject() {
|
|
18335
|
+
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
18336
|
+
return super.toObject([...TRIANGLE_PROPS, ...propertiesToInclude]);
|
|
18337
|
+
}
|
|
18338
|
+
|
|
17874
18339
|
/**
|
|
17875
18340
|
* Returns svg representation of an instance
|
|
17876
18341
|
* @return {Array} an array of strings with the specific svg representation
|
|
17877
18342
|
* of the instance
|
|
17878
18343
|
*/
|
|
17879
18344
|
_toSVG() {
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
points =
|
|
17883
|
-
|
|
18345
|
+
if (this.cornerRadius > 0) {
|
|
18346
|
+
// Generate rounded triangle as path
|
|
18347
|
+
const points = this._getTrianglePoints();
|
|
18348
|
+
const roundedCorners = applyCornerRadiusToPolygon(points, this.cornerRadius);
|
|
18349
|
+
const pathData = generateRoundedPolygonPath(roundedCorners, true);
|
|
18350
|
+
return ['<path ', 'COMMON_PARTS', `d="${pathData}" />`];
|
|
18351
|
+
} else {
|
|
18352
|
+
// Original sharp triangle implementation
|
|
18353
|
+
const widthBy2 = this.width / 2;
|
|
18354
|
+
const heightBy2 = this.height / 2;
|
|
18355
|
+
const points = `${-widthBy2} ${heightBy2},0 ${-heightBy2},${widthBy2} ${heightBy2}`;
|
|
18356
|
+
return ['<polygon ', 'COMMON_PARTS', 'points="', points, '" />'];
|
|
18357
|
+
}
|
|
17884
18358
|
}
|
|
17885
18359
|
}
|
|
18360
|
+
/**
|
|
18361
|
+
* Corner radius for rounded triangle corners
|
|
18362
|
+
* @type Number
|
|
18363
|
+
*/
|
|
17886
18364
|
_defineProperty(Triangle, "type", 'Triangle');
|
|
18365
|
+
_defineProperty(Triangle, "cacheProperties", [...cacheProperties, ...TRIANGLE_PROPS]);
|
|
17887
18366
|
_defineProperty(Triangle, "ownDefaults", triangleDefaultValues);
|
|
17888
18367
|
classRegistry.setClass(Triangle);
|
|
17889
18368
|
classRegistry.setSVGClass(Triangle);
|
|
@@ -18048,7 +18527,8 @@ const polylineDefaultValues = {
|
|
|
18048
18527
|
/**
|
|
18049
18528
|
* @deprecated transient option soon to be removed in favor of a different design
|
|
18050
18529
|
*/
|
|
18051
|
-
exactBoundingBox: false
|
|
18530
|
+
exactBoundingBox: false,
|
|
18531
|
+
cornerRadius: 0
|
|
18052
18532
|
};
|
|
18053
18533
|
class Polyline extends FabricObject {
|
|
18054
18534
|
static getDefaults() {
|
|
@@ -18262,7 +18742,7 @@ class Polyline extends FabricObject {
|
|
|
18262
18742
|
toObject() {
|
|
18263
18743
|
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
18264
18744
|
return {
|
|
18265
|
-
...super.toObject(propertiesToInclude),
|
|
18745
|
+
...super.toObject(['cornerRadius', ...propertiesToInclude]),
|
|
18266
18746
|
points: this.points.map(_ref => {
|
|
18267
18747
|
let {
|
|
18268
18748
|
x,
|
|
@@ -18282,14 +18762,28 @@ class Polyline extends FabricObject {
|
|
|
18282
18762
|
* of the instance
|
|
18283
18763
|
*/
|
|
18284
18764
|
_toSVG() {
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
|
|
18290
|
-
|
|
18765
|
+
if (this.cornerRadius > 0 && this.points.length >= 3) {
|
|
18766
|
+
// Generate rounded polygon/polyline as path
|
|
18767
|
+
const diffX = this.pathOffset.x;
|
|
18768
|
+
const diffY = this.pathOffset.y;
|
|
18769
|
+
const adjustedPoints = this.points.map(point => ({
|
|
18770
|
+
x: point.x - diffX,
|
|
18771
|
+
y: point.y - diffY
|
|
18772
|
+
}));
|
|
18773
|
+
const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
|
|
18774
|
+
const pathData = generateRoundedPolygonPath(roundedCorners, !this.isOpen());
|
|
18775
|
+
return ['<path ', 'COMMON_PARTS', `d="${pathData}" />\n`];
|
|
18776
|
+
} else {
|
|
18777
|
+
// Original sharp corners implementation
|
|
18778
|
+
const points = [];
|
|
18779
|
+
const diffX = this.pathOffset.x;
|
|
18780
|
+
const diffY = this.pathOffset.y;
|
|
18781
|
+
const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS;
|
|
18782
|
+
for (let i = 0, len = this.points.length; i < len; i++) {
|
|
18783
|
+
points.push(toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ');
|
|
18784
|
+
}
|
|
18785
|
+
return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
|
|
18291
18786
|
}
|
|
18292
|
-
return [`<${this.constructor.type.toLowerCase()} `, 'COMMON_PARTS', `points="${points.join('')}" />\n`];
|
|
18293
18787
|
}
|
|
18294
18788
|
|
|
18295
18789
|
/**
|
|
@@ -18305,13 +18799,24 @@ class Polyline extends FabricObject {
|
|
|
18305
18799
|
// NaN comes from parseFloat of a empty string in parser
|
|
18306
18800
|
return;
|
|
18307
18801
|
}
|
|
18308
|
-
|
|
18309
|
-
|
|
18310
|
-
|
|
18311
|
-
|
|
18312
|
-
|
|
18802
|
+
if (this.cornerRadius > 0 && len >= 3) {
|
|
18803
|
+
// Render with rounded corners
|
|
18804
|
+
const adjustedPoints = this.points.map(point => ({
|
|
18805
|
+
x: point.x - x,
|
|
18806
|
+
y: point.y - y
|
|
18807
|
+
}));
|
|
18808
|
+
const roundedCorners = applyCornerRadiusToPolygon(adjustedPoints, this.cornerRadius);
|
|
18809
|
+
renderRoundedPolygon(ctx, roundedCorners, !this.isOpen());
|
|
18810
|
+
} else {
|
|
18811
|
+
// Original sharp corners implementation
|
|
18812
|
+
ctx.beginPath();
|
|
18813
|
+
ctx.moveTo(this.points[0].x - x, this.points[0].y - y);
|
|
18814
|
+
for (let i = 0; i < len; i++) {
|
|
18815
|
+
const point = this.points[i];
|
|
18816
|
+
ctx.lineTo(point.x - x, point.y - y);
|
|
18817
|
+
}
|
|
18818
|
+
!this.isOpen() && ctx.closePath();
|
|
18313
18819
|
}
|
|
18314
|
-
!this.isOpen() && ctx.closePath();
|
|
18315
18820
|
this._renderPaintInOrder(ctx);
|
|
18316
18821
|
}
|
|
18317
18822
|
|
|
@@ -18376,10 +18881,15 @@ class Polyline extends FabricObject {
|
|
|
18376
18881
|
* @type Boolean
|
|
18377
18882
|
* @default false
|
|
18378
18883
|
*/
|
|
18884
|
+
/**
|
|
18885
|
+
* Corner radius for rounded corners
|
|
18886
|
+
* @type Number
|
|
18887
|
+
* @default 0
|
|
18888
|
+
*/
|
|
18379
18889
|
_defineProperty(Polyline, "ownDefaults", polylineDefaultValues);
|
|
18380
18890
|
_defineProperty(Polyline, "type", 'Polyline');
|
|
18381
18891
|
_defineProperty(Polyline, "layoutProperties", [SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points']);
|
|
18382
|
-
_defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points']);
|
|
18892
|
+
_defineProperty(Polyline, "cacheProperties", [...cacheProperties, 'points', 'cornerRadius']);
|
|
18383
18893
|
_defineProperty(Polyline, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES]);
|
|
18384
18894
|
classRegistry.setClass(Polyline);
|
|
18385
18895
|
classRegistry.setSVGClass(Polyline);
|
|
@@ -18763,6 +19273,97 @@ function measureGraphemeWithKerning(grapheme, previousGrapheme, options, ctx) {
|
|
|
18763
19273
|
};
|
|
18764
19274
|
}
|
|
18765
19275
|
|
|
19276
|
+
/**
|
|
19277
|
+
* Get a representative character for font metrics measurement
|
|
19278
|
+
* Uses canvas to test which scripts the font actually supports
|
|
19279
|
+
*/
|
|
19280
|
+
function getRepresentativeCharacter(fontFamily) {
|
|
19281
|
+
const context = getMeasurementContext();
|
|
19282
|
+
|
|
19283
|
+
// Wait for font to be ready if possible
|
|
19284
|
+
if (typeof document !== 'undefined' && 'fonts' in document) {
|
|
19285
|
+
try {
|
|
19286
|
+
// Check if font is ready, if not, use fallback immediately
|
|
19287
|
+
if (!document.fonts.check(`16px ${fontFamily}`)) {
|
|
19288
|
+
return 'M'; // Use safe fallback while font loads
|
|
19289
|
+
}
|
|
19290
|
+
} catch (e) {
|
|
19291
|
+
// Font check failed, use fallback
|
|
19292
|
+
return 'M';
|
|
19293
|
+
}
|
|
19294
|
+
}
|
|
19295
|
+
|
|
19296
|
+
// Test characters for different scripts
|
|
19297
|
+
const testChars = [{
|
|
19298
|
+
char: 'م',
|
|
19299
|
+
script: 'Arabic'
|
|
19300
|
+
},
|
|
19301
|
+
// Arabic
|
|
19302
|
+
{
|
|
19303
|
+
char: 'א',
|
|
19304
|
+
script: 'Hebrew'
|
|
19305
|
+
},
|
|
19306
|
+
// Hebrew
|
|
19307
|
+
{
|
|
19308
|
+
char: 'अ',
|
|
19309
|
+
script: 'Devanagari'
|
|
19310
|
+
},
|
|
19311
|
+
// Hindi/Sanskrit
|
|
19312
|
+
{
|
|
19313
|
+
char: 'ا',
|
|
19314
|
+
script: 'Urdu'
|
|
19315
|
+
},
|
|
19316
|
+
// Urdu
|
|
19317
|
+
{
|
|
19318
|
+
char: 'ک',
|
|
19319
|
+
script: 'Persian'
|
|
19320
|
+
},
|
|
19321
|
+
// Persian
|
|
19322
|
+
{
|
|
19323
|
+
char: 'த',
|
|
19324
|
+
script: 'Tamil'
|
|
19325
|
+
},
|
|
19326
|
+
// Tamil
|
|
19327
|
+
{
|
|
19328
|
+
char: 'ก',
|
|
19329
|
+
script: 'Thai'
|
|
19330
|
+
},
|
|
19331
|
+
// Thai
|
|
19332
|
+
{
|
|
19333
|
+
char: 'М',
|
|
19334
|
+
script: 'Cyrillic'
|
|
19335
|
+
},
|
|
19336
|
+
// Cyrillic
|
|
19337
|
+
{
|
|
19338
|
+
char: 'Ω',
|
|
19339
|
+
script: 'Greek'
|
|
19340
|
+
},
|
|
19341
|
+
// Greek
|
|
19342
|
+
{
|
|
19343
|
+
char: 'M',
|
|
19344
|
+
script: 'Latin'
|
|
19345
|
+
} // Latin (fallback)
|
|
19346
|
+
];
|
|
19347
|
+
|
|
19348
|
+
// Set the font
|
|
19349
|
+
context.font = `16px ${fontFamily}`;
|
|
19350
|
+
|
|
19351
|
+
// Test each character to see which ones render properly
|
|
19352
|
+
// Use a more robust width check to avoid false positives
|
|
19353
|
+
const fallbackWidth = context.measureText('M').width;
|
|
19354
|
+
for (const test of testChars) {
|
|
19355
|
+
const metrics = context.measureText(test.char);
|
|
19356
|
+
|
|
19357
|
+
// Character is valid if it has width and isn't just a fallback glyph
|
|
19358
|
+
if (metrics.width > 0 && Math.abs(metrics.width - fallbackWidth) > 0.1) {
|
|
19359
|
+
return test.char;
|
|
19360
|
+
}
|
|
19361
|
+
}
|
|
19362
|
+
|
|
19363
|
+
// Fallback to Latin 'M'
|
|
19364
|
+
return 'M';
|
|
19365
|
+
}
|
|
19366
|
+
|
|
18766
19367
|
/**
|
|
18767
19368
|
* Get font metrics for layout calculations
|
|
18768
19369
|
*/
|
|
@@ -18776,8 +19377,9 @@ function getFontMetrics(options) {
|
|
|
18776
19377
|
const context = getMeasurementContext();
|
|
18777
19378
|
applyFontStyle(context, options);
|
|
18778
19379
|
|
|
18779
|
-
// Use
|
|
18780
|
-
const
|
|
19380
|
+
// Use representative character based on font's primary script
|
|
19381
|
+
const sample = getRepresentativeCharacter(options.fontFamily);
|
|
19382
|
+
const metrics = context.measureText(sample);
|
|
18781
19383
|
const fontSize = options.fontSize;
|
|
18782
19384
|
|
|
18783
19385
|
// Calculate metrics with fallbacks
|
|
@@ -18829,7 +19431,11 @@ function getFontDeclaration(options) {
|
|
|
18829
19431
|
} = options;
|
|
18830
19432
|
|
|
18831
19433
|
// Normalize font family (add quotes if needed)
|
|
18832
|
-
|
|
19434
|
+
let normalizedFamily = fontFamily.includes(' ') && !fontFamily.includes('"') && !fontFamily.includes("'") ? `"${fontFamily}"` : fontFamily;
|
|
19435
|
+
|
|
19436
|
+
// Note: Font fallbacks are handled in the rendering phase only
|
|
19437
|
+
// to avoid affecting measurement calculations for text wrapping
|
|
19438
|
+
|
|
18833
19439
|
return `${fontStyle} ${fontWeight} ${fontSize}px ${normalizedFamily}`;
|
|
18834
19440
|
}
|
|
18835
19441
|
|
|
@@ -18981,6 +19587,81 @@ const measurementCache = new MeasurementCache();
|
|
|
18981
19587
|
const kerningCache = new KerningCache();
|
|
18982
19588
|
const fontMetricsCache = new FontMetricsCache();
|
|
18983
19589
|
|
|
19590
|
+
// Set up font loading listener to clear caches when fonts change
|
|
19591
|
+
if (typeof document !== 'undefined' && 'fonts' in document) {
|
|
19592
|
+
document.fonts.addEventListener('loadingdone', () => {
|
|
19593
|
+
// Clear all caches when fonts finish loading
|
|
19594
|
+
clearAllCaches();
|
|
19595
|
+
});
|
|
19596
|
+
}
|
|
19597
|
+
|
|
19598
|
+
/**
|
|
19599
|
+
* Clear all measurement caches
|
|
19600
|
+
*/
|
|
19601
|
+
function clearAllCaches() {
|
|
19602
|
+
measurementCache.clear();
|
|
19603
|
+
kerningCache.clear();
|
|
19604
|
+
fontMetricsCache.clear();
|
|
19605
|
+
}
|
|
19606
|
+
|
|
19607
|
+
/**
|
|
19608
|
+
* Detect if a font lacks English glyph support
|
|
19609
|
+
* These fonts should use browser-native measurement instead of Fabric's character-by-character measurement
|
|
19610
|
+
*/
|
|
19611
|
+
function fontLacksEnglishGlyphs(fontFamily) {
|
|
19612
|
+
if (typeof document === 'undefined') return false;
|
|
19613
|
+
|
|
19614
|
+
// Known fonts that lack English glyphs
|
|
19615
|
+
const knownNonEnglishFonts = ['stv', 'arabic', 'naskh', 'thuluth', 'kufi', 'diwani', 'nastaliq', 'kufic', 'hijazi', 'madinah', 'makkah'];
|
|
19616
|
+
const lowerFontFamily = fontFamily.toLowerCase();
|
|
19617
|
+
|
|
19618
|
+
// Check known list first
|
|
19619
|
+
if (knownNonEnglishFonts.some(font => lowerFontFamily.includes(font))) {
|
|
19620
|
+
return true;
|
|
19621
|
+
}
|
|
19622
|
+
|
|
19623
|
+
// Dynamic glyph support detection
|
|
19624
|
+
const context = getMeasurementContext();
|
|
19625
|
+
context.font = `16px ${fontFamily}`;
|
|
19626
|
+
|
|
19627
|
+
// Test English characters
|
|
19628
|
+
const englishChars = ['A', 'B', 'C', 'a', 'b', 'c', 'M', 'W'];
|
|
19629
|
+
const fallbackFont = 'Arial, sans-serif';
|
|
19630
|
+
|
|
19631
|
+
// Measure with target font
|
|
19632
|
+
const targetWidths = englishChars.map(char => context.measureText(char).width);
|
|
19633
|
+
|
|
19634
|
+
// Measure with fallback font
|
|
19635
|
+
context.font = `16px ${fallbackFont}`;
|
|
19636
|
+
const fallbackWidths = englishChars.map(char => context.measureText(char).width);
|
|
19637
|
+
|
|
19638
|
+
// If most measurements are identical, the font likely doesn't have English glyphs
|
|
19639
|
+
let identicalCount = 0;
|
|
19640
|
+
for (let i = 0; i < englishChars.length; i++) {
|
|
19641
|
+
if (Math.abs(targetWidths[i] - fallbackWidths[i]) < 0.5) {
|
|
19642
|
+
identicalCount++;
|
|
19643
|
+
}
|
|
19644
|
+
}
|
|
19645
|
+
const lacksSupportThreshold = englishChars.length * 0.7; // 70% identical = lacks support
|
|
19646
|
+
const lacksSupport = identicalCount >= lacksSupportThreshold;
|
|
19647
|
+
return lacksSupport;
|
|
19648
|
+
}
|
|
19649
|
+
|
|
19650
|
+
// Cache for font glyph detection results
|
|
19651
|
+
const fontGlyphCache = new Map();
|
|
19652
|
+
|
|
19653
|
+
/**
|
|
19654
|
+
* Cached version of font glyph detection
|
|
19655
|
+
*/
|
|
19656
|
+
function fontLacksEnglishGlyphsCached(fontFamily) {
|
|
19657
|
+
if (fontGlyphCache.has(fontFamily)) {
|
|
19658
|
+
return fontGlyphCache.get(fontFamily);
|
|
19659
|
+
}
|
|
19660
|
+
const result = fontLacksEnglishGlyphs(fontFamily);
|
|
19661
|
+
fontGlyphCache.set(fontFamily, result);
|
|
19662
|
+
return result;
|
|
19663
|
+
}
|
|
19664
|
+
|
|
18984
19665
|
/**
|
|
18985
19666
|
* Unicode and Internationalization Support
|
|
18986
19667
|
*
|
|
@@ -20164,6 +20845,15 @@ class FabricText extends StyledText {
|
|
|
20164
20845
|
* Does not return dimensions.
|
|
20165
20846
|
*/
|
|
20166
20847
|
initDimensions() {
|
|
20848
|
+
// Check if font is ready for accurate measurements
|
|
20849
|
+
// Only block initialization if it's a critical font loading situation
|
|
20850
|
+
const fontReady = this._isFontReady();
|
|
20851
|
+
if (!fontReady && !this.initialized) {
|
|
20852
|
+
// Only schedule font loading on first initialization
|
|
20853
|
+
this._scheduleInitAfterFontLoad();
|
|
20854
|
+
// Continue with fallback measurements for now
|
|
20855
|
+
}
|
|
20856
|
+
|
|
20167
20857
|
// Use advanced layout if enabled
|
|
20168
20858
|
if (this.enableAdvancedLayout && !this.path) {
|
|
20169
20859
|
return this.initDimensionsAdvanced();
|
|
@@ -20180,7 +20870,21 @@ class FabricText extends StyledText {
|
|
|
20180
20870
|
}
|
|
20181
20871
|
if (this.textAlign.includes(JUSTIFY)) {
|
|
20182
20872
|
// once text is measured we need to make space fatter to make justified text.
|
|
20183
|
-
|
|
20873
|
+
// Ensure __charBounds exists before calling enlargeSpaces
|
|
20874
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
20875
|
+
this.enlargeSpaces();
|
|
20876
|
+
} else {
|
|
20877
|
+
console.warn('⚠️ __charBounds not ready for justify alignment, deferring enlargeSpaces');
|
|
20878
|
+
// Defer the justify calculation until the next frame
|
|
20879
|
+
setTimeout(() => {
|
|
20880
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
20881
|
+
var _this$canvas;
|
|
20882
|
+
console.log('🔧 Applying deferred justify alignment');
|
|
20883
|
+
this.enlargeSpaces();
|
|
20884
|
+
(_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
|
|
20885
|
+
}
|
|
20886
|
+
}, 0);
|
|
20887
|
+
}
|
|
20184
20888
|
}
|
|
20185
20889
|
}
|
|
20186
20890
|
|
|
@@ -20189,8 +20893,9 @@ class FabricText extends StyledText {
|
|
|
20189
20893
|
*/
|
|
20190
20894
|
enlargeSpaces() {
|
|
20191
20895
|
let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
|
|
20896
|
+
const isRtl = this.direction === 'rtl';
|
|
20192
20897
|
for (let i = 0, len = this._textLines.length; i < len; i++) {
|
|
20193
|
-
if (this.textAlign
|
|
20898
|
+
if (!this.textAlign.includes('justify') && (i === len - 1 || this.isEndOfWrapping(i))) {
|
|
20194
20899
|
continue;
|
|
20195
20900
|
}
|
|
20196
20901
|
accumulatedSpace = 0;
|
|
@@ -20199,15 +20904,47 @@ class FabricText extends StyledText {
|
|
|
20199
20904
|
if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
|
|
20200
20905
|
numberOfSpaces = spaces.length;
|
|
20201
20906
|
diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
|
|
20202
|
-
|
|
20203
|
-
|
|
20204
|
-
|
|
20205
|
-
|
|
20206
|
-
|
|
20207
|
-
|
|
20208
|
-
|
|
20209
|
-
|
|
20210
|
-
|
|
20907
|
+
console.log(`🔧 EnlargeSpaces Line ${i}:`);
|
|
20908
|
+
console.log(` Current width: ${currentLineWidth}, Target: ${this.width}`);
|
|
20909
|
+
console.log(` Spaces: ${numberOfSpaces}, diffSpace: ${diffSpace.toFixed(2)}`);
|
|
20910
|
+
if (isRtl) {
|
|
20911
|
+
for (let j = 0; j < line.length; j++) {
|
|
20912
|
+
if (this._reSpaceAndTab.test(line[j])) ;
|
|
20913
|
+
}
|
|
20914
|
+
|
|
20915
|
+
// For RTL, we need to work backwards through the visual positions
|
|
20916
|
+
// but still update logical positions correctly
|
|
20917
|
+
let spaceCount = 0;
|
|
20918
|
+
for (let j = 0; j <= line.length; j++) {
|
|
20919
|
+
charBound = this.__charBounds[i][j];
|
|
20920
|
+
if (charBound) {
|
|
20921
|
+
if (this._reSpaceAndTab.test(line[j])) {
|
|
20922
|
+
charBound.width += diffSpace;
|
|
20923
|
+
charBound.kernedWidth += diffSpace;
|
|
20924
|
+
spaceCount++;
|
|
20925
|
+
}
|
|
20926
|
+
|
|
20927
|
+
// For RTL, shift all characters to the right by the total expansion
|
|
20928
|
+
// minus the expansion that comes after this character
|
|
20929
|
+
const remainingSpaces = numberOfSpaces - spaceCount;
|
|
20930
|
+
const shiftAmount = remainingSpaces * diffSpace;
|
|
20931
|
+
charBound.left += shiftAmount;
|
|
20932
|
+
}
|
|
20933
|
+
}
|
|
20934
|
+
} else {
|
|
20935
|
+
// LTR processing (original logic)
|
|
20936
|
+
for (let j = 0; j <= line.length; j++) {
|
|
20937
|
+
charBound = this.__charBounds[i][j];
|
|
20938
|
+
if (charBound) {
|
|
20939
|
+
if (this._reSpaceAndTab.test(line[j])) {
|
|
20940
|
+
charBound.width += diffSpace;
|
|
20941
|
+
charBound.kernedWidth += diffSpace;
|
|
20942
|
+
charBound.left += accumulatedSpace;
|
|
20943
|
+
accumulatedSpace += diffSpace;
|
|
20944
|
+
} else {
|
|
20945
|
+
charBound.left += accumulatedSpace;
|
|
20946
|
+
}
|
|
20947
|
+
}
|
|
20211
20948
|
}
|
|
20212
20949
|
}
|
|
20213
20950
|
}
|
|
@@ -20285,6 +21022,18 @@ class FabricText extends StyledText {
|
|
|
20285
21022
|
|
|
20286
21023
|
// Convert layout to legacy format for compatibility
|
|
20287
21024
|
this._convertLayoutToLegacyFormat(layout);
|
|
21025
|
+
|
|
21026
|
+
// Ensure justify alignment is properly applied for compatibility with legacy rendering
|
|
21027
|
+
if (this.textAlign.includes(JUSTIFY)) {
|
|
21028
|
+
// Force enlarge spaces after advanced layout calculation
|
|
21029
|
+
setTimeout(() => {
|
|
21030
|
+
if (this.enlargeSpaces) {
|
|
21031
|
+
var _this$canvas2;
|
|
21032
|
+
this.enlargeSpaces();
|
|
21033
|
+
(_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.renderAll();
|
|
21034
|
+
}
|
|
21035
|
+
}, 0);
|
|
21036
|
+
}
|
|
20288
21037
|
this.dirty = true;
|
|
20289
21038
|
}
|
|
20290
21039
|
|
|
@@ -20865,7 +21614,15 @@ class FabricText extends StyledText {
|
|
|
20865
21614
|
if (currentDirection !== this.direction) {
|
|
20866
21615
|
ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
|
|
20867
21616
|
ctx.direction = isLtr ? 'ltr' : 'rtl';
|
|
20868
|
-
|
|
21617
|
+
|
|
21618
|
+
// For justify alignments, we need to set the correct canvas text alignment
|
|
21619
|
+
// This is crucial for RTL text to render in the correct order
|
|
21620
|
+
if (isJustify) {
|
|
21621
|
+
// Justify uses LEFT alignment as a base, letting the character positioning handle justification
|
|
21622
|
+
ctx.textAlign = LEFT;
|
|
21623
|
+
} else {
|
|
21624
|
+
ctx.textAlign = isLtr ? LEFT : RIGHT;
|
|
21625
|
+
}
|
|
20869
21626
|
}
|
|
20870
21627
|
top -= lineHeight * this._fontSizeFraction / this.lineHeight;
|
|
20871
21628
|
if (shortCut) {
|
|
@@ -21101,9 +21858,21 @@ class FabricText extends StyledText {
|
|
|
21101
21858
|
direction = this.direction,
|
|
21102
21859
|
isEndOfWrapping = this.isEndOfWrapping(lineIndex);
|
|
21103
21860
|
let leftOffset = 0;
|
|
21104
|
-
|
|
21105
|
-
|
|
21861
|
+
|
|
21862
|
+
// Handle justify alignments (excluding last lines and wrapped line ends)
|
|
21863
|
+
const isJustifyLine = textAlign === JUSTIFY || textAlign === JUSTIFY_CENTER && !isEndOfWrapping || textAlign === JUSTIFY_RIGHT && !isEndOfWrapping || textAlign === JUSTIFY_LEFT && !isEndOfWrapping;
|
|
21864
|
+
if (isJustifyLine) {
|
|
21865
|
+
// Justify lines should start at the left edge for LTR and right edge for RTL
|
|
21866
|
+
// The space distribution is handled by enlargeSpaces()
|
|
21867
|
+
if (direction === 'rtl') {
|
|
21868
|
+
// For RTL justify, we need to account for the line being right-aligned
|
|
21869
|
+
return 0;
|
|
21870
|
+
} else {
|
|
21871
|
+
return 0;
|
|
21872
|
+
}
|
|
21106
21873
|
}
|
|
21874
|
+
|
|
21875
|
+
// Handle non-justify alignments
|
|
21107
21876
|
if (textAlign === CENTER) {
|
|
21108
21877
|
leftOffset = lineDiff / 2;
|
|
21109
21878
|
}
|
|
@@ -21116,6 +21885,8 @@ class FabricText extends StyledText {
|
|
|
21116
21885
|
if (textAlign === JUSTIFY_RIGHT) {
|
|
21117
21886
|
leftOffset = lineDiff;
|
|
21118
21887
|
}
|
|
21888
|
+
|
|
21889
|
+
// Apply RTL adjustments for non-justify alignments
|
|
21119
21890
|
if (direction === 'rtl') {
|
|
21120
21891
|
if (textAlign === RIGHT || textAlign === JUSTIFY || textAlign === JUSTIFY_RIGHT) {
|
|
21121
21892
|
leftOffset = 0;
|
|
@@ -21274,7 +22045,19 @@ class FabricText extends StyledText {
|
|
|
21274
22045
|
fontSize = this.fontSize
|
|
21275
22046
|
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
21276
22047
|
let forMeasuring = arguments.length > 1 ? arguments[1] : undefined;
|
|
21277
|
-
|
|
22048
|
+
let parsedFontFamily = fontFamily.includes("'") || fontFamily.includes('"') || fontFamily.includes(',') || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`;
|
|
22049
|
+
|
|
22050
|
+
// For fonts like STV that don't support English/Latin characters,
|
|
22051
|
+
// add fallback fonts for consistent rendering of unsupported characters
|
|
22052
|
+
// Only add fallbacks during actual rendering, not for measurements
|
|
22053
|
+
if (!forMeasuring &&
|
|
22054
|
+
// Only during rendering, not measuring
|
|
22055
|
+
!fontFamily.includes(',') && (
|
|
22056
|
+
// Don't add fallbacks if already has them
|
|
22057
|
+
fontFamily.toLowerCase().includes('stv') || fontFamily.toLowerCase().includes('arabic') || fontFamily.toLowerCase().includes('naskh') || fontFamily.toLowerCase().includes('kufi'))) {
|
|
22058
|
+
// Add fallback fonts for unsupported characters (spaces, punctuation, etc.)
|
|
22059
|
+
parsedFontFamily = `${parsedFontFamily}, "Arial Unicode MS", Arial, sans-serif`;
|
|
22060
|
+
}
|
|
21278
22061
|
return [fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily].join(' ');
|
|
21279
22062
|
}
|
|
21280
22063
|
|
|
@@ -21318,7 +22101,13 @@ class FabricText extends StyledText {
|
|
|
21318
22101
|
newLine = ['\n'];
|
|
21319
22102
|
let newText = [];
|
|
21320
22103
|
for (let i = 0; i < lines.length; i++) {
|
|
21321
|
-
|
|
22104
|
+
// Use BiDi-aware grapheme splitting for RTL text
|
|
22105
|
+
if (this.direction === 'rtl' || this._containsArabicText(lines[i])) {
|
|
22106
|
+
newLines[i] = segmentGraphemes(lines[i]);
|
|
22107
|
+
console.log(`🔤 BiDi-aware split line ${i}: "${lines[i]}" -> [${newLines[i].join(', ')}]`);
|
|
22108
|
+
} else {
|
|
22109
|
+
newLines[i] = this.graphemeSplit(lines[i]);
|
|
22110
|
+
}
|
|
21322
22111
|
newText = newText.concat(newLines[i], newLine);
|
|
21323
22112
|
}
|
|
21324
22113
|
newText.pop();
|
|
@@ -21330,6 +22119,14 @@ class FabricText extends StyledText {
|
|
|
21330
22119
|
};
|
|
21331
22120
|
}
|
|
21332
22121
|
|
|
22122
|
+
/**
|
|
22123
|
+
* Check if text contains Arabic characters
|
|
22124
|
+
* @private
|
|
22125
|
+
*/
|
|
22126
|
+
_containsArabicText(text) {
|
|
22127
|
+
return /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
22128
|
+
}
|
|
22129
|
+
|
|
21333
22130
|
/**
|
|
21334
22131
|
* Returns object representation of an instance
|
|
21335
22132
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
@@ -21439,18 +22236,100 @@ class FabricText extends StyledText {
|
|
|
21439
22236
|
if (textAnchor === CENTER) {
|
|
21440
22237
|
offX = text.getScaledWidth() / 2;
|
|
21441
22238
|
}
|
|
21442
|
-
if (textAnchor === RIGHT) {
|
|
21443
|
-
offX = text.getScaledWidth();
|
|
22239
|
+
if (textAnchor === RIGHT) {
|
|
22240
|
+
offX = text.getScaledWidth();
|
|
22241
|
+
}
|
|
22242
|
+
text.set({
|
|
22243
|
+
left: text.left - offX,
|
|
22244
|
+
top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight,
|
|
22245
|
+
strokeWidth
|
|
22246
|
+
});
|
|
22247
|
+
return text;
|
|
22248
|
+
}
|
|
22249
|
+
|
|
22250
|
+
/* _FROM_SVG_END_ */
|
|
22251
|
+
|
|
22252
|
+
/**
|
|
22253
|
+
* Check if the font is ready for accurate measurements
|
|
22254
|
+
* @private
|
|
22255
|
+
*/
|
|
22256
|
+
_isFontReady() {
|
|
22257
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
22258
|
+
return true; // Assume ready in non-browser environments
|
|
22259
|
+
}
|
|
22260
|
+
try {
|
|
22261
|
+
return document.fonts.check(`${this.fontSize}px ${this.fontFamily}`);
|
|
22262
|
+
} catch (e) {
|
|
22263
|
+
return true; // Fallback to assuming ready if check fails
|
|
22264
|
+
}
|
|
22265
|
+
}
|
|
22266
|
+
|
|
22267
|
+
/**
|
|
22268
|
+
* Schedule re-initialization after font loads
|
|
22269
|
+
* @private
|
|
22270
|
+
*/
|
|
22271
|
+
_scheduleInitAfterFontLoad() {
|
|
22272
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
22273
|
+
return;
|
|
22274
|
+
}
|
|
22275
|
+
|
|
22276
|
+
// Only schedule if not already waiting
|
|
22277
|
+
if (this._fontLoadScheduled) {
|
|
22278
|
+
return;
|
|
21444
22279
|
}
|
|
21445
|
-
|
|
21446
|
-
|
|
21447
|
-
|
|
21448
|
-
|
|
22280
|
+
this._fontLoadScheduled = true;
|
|
22281
|
+
const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
|
|
22282
|
+
document.fonts.load(fontSpec).then(() => {
|
|
22283
|
+
this._fontLoadScheduled = false;
|
|
22284
|
+
// Re-initialize dimensions with proper font metrics
|
|
22285
|
+
this.initDimensions();
|
|
22286
|
+
|
|
22287
|
+
// Extra step for justify alignment after font loading
|
|
22288
|
+
if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
|
|
22289
|
+
setTimeout(() => {
|
|
22290
|
+
var _this$canvas3;
|
|
22291
|
+
if (this.enlargeSpaces) {
|
|
22292
|
+
this.enlargeSpaces();
|
|
22293
|
+
}
|
|
22294
|
+
(_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
|
|
22295
|
+
}, 10);
|
|
22296
|
+
} else {
|
|
22297
|
+
var _this$canvas4;
|
|
22298
|
+
(_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.requestRenderAll();
|
|
22299
|
+
}
|
|
22300
|
+
}).catch(() => {
|
|
22301
|
+
this._fontLoadScheduled = false;
|
|
21449
22302
|
});
|
|
21450
|
-
return text;
|
|
21451
22303
|
}
|
|
21452
22304
|
|
|
21453
|
-
|
|
22305
|
+
/**
|
|
22306
|
+
* Force complete text re-initialization (useful after JSON loading)
|
|
22307
|
+
*/
|
|
22308
|
+
forceTextReinitialization() {
|
|
22309
|
+
console.log('🔄 Force reinitializing text object');
|
|
22310
|
+
|
|
22311
|
+
// Clear all caches
|
|
22312
|
+
this._clearCache();
|
|
22313
|
+
this.dirty = true;
|
|
22314
|
+
|
|
22315
|
+
// Force text splitting to rebuild internal structures
|
|
22316
|
+
this._splitText();
|
|
22317
|
+
|
|
22318
|
+
// Re-initialize dimensions
|
|
22319
|
+
this.initDimensions();
|
|
22320
|
+
|
|
22321
|
+
// Special handling for justify alignment
|
|
22322
|
+
if (this.textAlign && this.textAlign.includes(JUSTIFY)) {
|
|
22323
|
+
// Ensure justify is applied after dimensions are set
|
|
22324
|
+
setTimeout(() => {
|
|
22325
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
22326
|
+
var _this$canvas5;
|
|
22327
|
+
this.enlargeSpaces();
|
|
22328
|
+
(_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
|
|
22329
|
+
}
|
|
22330
|
+
}, 10);
|
|
22331
|
+
}
|
|
22332
|
+
}
|
|
21454
22333
|
|
|
21455
22334
|
/**
|
|
21456
22335
|
* Returns FabricText instance from an object representation
|
|
@@ -21463,6 +22342,93 @@ class FabricText extends StyledText {
|
|
|
21463
22342
|
styles: stylesFromArray(object.styles || {}, object.text)
|
|
21464
22343
|
}, {
|
|
21465
22344
|
extraParam: 'text'
|
|
22345
|
+
}).then(textObject => {
|
|
22346
|
+
// Ensure text object is properly initialized after JSON deserialization
|
|
22347
|
+
// This is critical for justify alignment and other text layout features
|
|
22348
|
+
textObject.initialized = true;
|
|
22349
|
+
|
|
22350
|
+
// Force reinitialization to ensure proper layout
|
|
22351
|
+
if (textObject._clearCache) {
|
|
22352
|
+
textObject._clearCache();
|
|
22353
|
+
}
|
|
22354
|
+
textObject.dirty = true;
|
|
22355
|
+
|
|
22356
|
+
// Check if we need to wait for font loading (especially for custom fonts like STV)
|
|
22357
|
+
const fontSpec = `${textObject.fontSize}px ${textObject.fontFamily}`;
|
|
22358
|
+
|
|
22359
|
+
// For custom fonts, ensure they're loaded before initializing dimensions
|
|
22360
|
+
if (typeof document !== 'undefined' && 'fonts' in document && textObject.fontFamily !== 'Arial' && textObject.fontFamily !== 'Times New Roman') {
|
|
22361
|
+
return document.fonts.load(fontSpec).then(() => {
|
|
22362
|
+
var _textObject$fontFamil;
|
|
22363
|
+
console.log(`🔤 Font loaded for JSON object: ${fontSpec}`);
|
|
22364
|
+
// Ensure initialized flag is set again (in case constructor reset it)
|
|
22365
|
+
textObject.initialized = true;
|
|
22366
|
+
|
|
22367
|
+
// Special handling for STV fonts which have measurement issues
|
|
22368
|
+
const isStvFont = (_textObject$fontFamil = textObject.fontFamily) === null || _textObject$fontFamil === void 0 ? void 0 : _textObject$fontFamil.toLowerCase().includes('stv');
|
|
22369
|
+
if (isStvFont) {
|
|
22370
|
+
console.log(`🔤 STV font detected, using enhanced reinitialization`);
|
|
22371
|
+
|
|
22372
|
+
// Clear all cached state that might interfere with browser wrapping
|
|
22373
|
+
textObject._browserWrapCache = null;
|
|
22374
|
+
textObject._lastDimensionState = null;
|
|
22375
|
+
textObject._browserWrapInitialized = false;
|
|
22376
|
+
console.log(`🔤 STV font: Cleared all cached states for fresh initialization`);
|
|
22377
|
+
|
|
22378
|
+
// Force browser wrapping flag for STV fonts
|
|
22379
|
+
textObject._usingBrowserWrapping = true;
|
|
22380
|
+
console.log(`🔤 STV font: Forcing browser wrapping flag during JSON load`);
|
|
22381
|
+
|
|
22382
|
+
// Multiple initialization attempts for STV fonts
|
|
22383
|
+
const reinitWithDelay = attempt => {
|
|
22384
|
+
if (textObject.forceTextReinitialization) {
|
|
22385
|
+
textObject.forceTextReinitialization();
|
|
22386
|
+
} else {
|
|
22387
|
+
textObject.initDimensions();
|
|
22388
|
+
}
|
|
22389
|
+
|
|
22390
|
+
// Check if width is still problematic after initialization
|
|
22391
|
+
if (textObject.width < 50 && attempt < 3) {
|
|
22392
|
+
console.log(`🔤 STV font width still ${textObject.width}px, retrying in ${100 * attempt}ms (attempt ${attempt + 1}/3)`);
|
|
22393
|
+
setTimeout(() => reinitWithDelay(attempt + 1), 100 * attempt);
|
|
22394
|
+
}
|
|
22395
|
+
};
|
|
22396
|
+
reinitWithDelay(0);
|
|
22397
|
+
} else {
|
|
22398
|
+
// Use specialized reinitialization for Textbox objects
|
|
22399
|
+
if (textObject.forceTextReinitialization) {
|
|
22400
|
+
console.log(`🔤 Using Textbox specialized reinitialization`);
|
|
22401
|
+
textObject.forceTextReinitialization();
|
|
22402
|
+
} else {
|
|
22403
|
+
// Reinitialize dimensions with proper font metrics
|
|
22404
|
+
textObject.initDimensions();
|
|
22405
|
+
}
|
|
22406
|
+
}
|
|
22407
|
+
return textObject;
|
|
22408
|
+
}).catch(() => {
|
|
22409
|
+
console.warn(`⚠️ Font loading failed for ${fontSpec}, proceeding with fallback`);
|
|
22410
|
+
// Ensure initialized flag is set again
|
|
22411
|
+
textObject.initialized = true;
|
|
22412
|
+
|
|
22413
|
+
// Still initialize dimensions even if font loading fails
|
|
22414
|
+
if (textObject.forceTextReinitialization) {
|
|
22415
|
+
textObject.forceTextReinitialization();
|
|
22416
|
+
} else {
|
|
22417
|
+
textObject.initDimensions();
|
|
22418
|
+
}
|
|
22419
|
+
return textObject;
|
|
22420
|
+
});
|
|
22421
|
+
} else {
|
|
22422
|
+
// Standard fonts - ensure initialized and use appropriate method
|
|
22423
|
+
textObject.initialized = true;
|
|
22424
|
+
if (textObject.forceTextReinitialization) {
|
|
22425
|
+
console.log(`🔤 Using Textbox specialized reinitialization for standard font`);
|
|
22426
|
+
textObject.forceTextReinitialization();
|
|
22427
|
+
} else {
|
|
22428
|
+
textObject.initDimensions();
|
|
22429
|
+
}
|
|
22430
|
+
return textObject;
|
|
22431
|
+
}
|
|
21466
22432
|
});
|
|
21467
22433
|
}
|
|
21468
22434
|
}
|
|
@@ -22106,18 +23072,98 @@ class OverlayEditor {
|
|
|
22106
23072
|
|
|
22107
23073
|
// Apply all other font and text styles to match Fabric
|
|
22108
23074
|
const letterSpacingPx = (target.charSpacing || 0) / 1000 * finalFontSize;
|
|
23075
|
+
|
|
23076
|
+
// Special handling for text objects loaded from JSON - ensure they're properly initialized
|
|
23077
|
+
if (target.dirty !== false && target.initDimensions) {
|
|
23078
|
+
console.log('🔧 Ensuring text object is properly initialized before overlay editing');
|
|
23079
|
+
// Force re-initialization if the text object seems to be in a dirty state
|
|
23080
|
+
target.initDimensions();
|
|
23081
|
+
}
|
|
22109
23082
|
this.textarea.style.fontSize = `${finalFontSize}px`;
|
|
22110
23083
|
this.textarea.style.lineHeight = String(fabricLineHeight);
|
|
22111
23084
|
this.textarea.style.fontFamily = target.fontFamily || 'Arial';
|
|
22112
23085
|
this.textarea.style.fontWeight = String(target.fontWeight || 'normal');
|
|
22113
23086
|
this.textarea.style.fontStyle = target.fontStyle || 'normal';
|
|
22114
|
-
|
|
23087
|
+
// Handle text alignment and justification
|
|
23088
|
+
const textAlign = target.textAlign || 'left';
|
|
23089
|
+
let cssTextAlign = textAlign;
|
|
23090
|
+
|
|
23091
|
+
// Detect text direction from content for proper justify handling
|
|
23092
|
+
const autoDetectedDirection = this.firstStrongDir(this.textarea.value || '');
|
|
23093
|
+
|
|
23094
|
+
// DEBUG: Log alignment details
|
|
23095
|
+
console.log('🔍 ALIGNMENT DEBUG:');
|
|
23096
|
+
console.log(' Fabric textAlign:', textAlign);
|
|
23097
|
+
console.log(' Fabric direction:', target.direction);
|
|
23098
|
+
console.log(' Text content:', JSON.stringify(target.text));
|
|
23099
|
+
console.log(' Detected direction:', autoDetectedDirection);
|
|
23100
|
+
|
|
23101
|
+
// Map fabric.js justify to CSS
|
|
23102
|
+
if (textAlign.includes('justify')) {
|
|
23103
|
+
// Try to match fabric.js justify behavior more precisely
|
|
23104
|
+
try {
|
|
23105
|
+
// For justify, we need to replicate fabric.js space expansion
|
|
23106
|
+
// Use CSS justify but with specific settings to match fabric.js better
|
|
23107
|
+
cssTextAlign = 'justify';
|
|
23108
|
+
|
|
23109
|
+
// Set text-align-last based on justify type and detected direction
|
|
23110
|
+
// Smart justify: respect detected direction even when fabric alignment doesn't match
|
|
23111
|
+
if (textAlign === 'justify') {
|
|
23112
|
+
this.textarea.style.textAlignLast = autoDetectedDirection === 'rtl' ? 'right' : 'left';
|
|
23113
|
+
} else if (textAlign === 'justify-left') {
|
|
23114
|
+
// If text is RTL but fabric says justify-left, override to justify-right for better UX
|
|
23115
|
+
if (autoDetectedDirection === 'rtl') {
|
|
23116
|
+
this.textarea.style.textAlignLast = 'right';
|
|
23117
|
+
console.log(' → Overrode justify-left to justify-right for RTL text');
|
|
23118
|
+
} else {
|
|
23119
|
+
this.textarea.style.textAlignLast = 'left';
|
|
23120
|
+
}
|
|
23121
|
+
} else if (textAlign === 'justify-right') {
|
|
23122
|
+
// If text is LTR but fabric says justify-right, override to justify-left for better UX
|
|
23123
|
+
if (autoDetectedDirection === 'ltr') {
|
|
23124
|
+
this.textarea.style.textAlignLast = 'left';
|
|
23125
|
+
console.log(' → Overrode justify-right to justify-left for LTR text');
|
|
23126
|
+
} else {
|
|
23127
|
+
this.textarea.style.textAlignLast = 'right';
|
|
23128
|
+
}
|
|
23129
|
+
} else if (textAlign === 'justify-center') {
|
|
23130
|
+
this.textarea.style.textAlignLast = 'center';
|
|
23131
|
+
}
|
|
23132
|
+
|
|
23133
|
+
// Enhanced justify settings for better fabric.js matching
|
|
23134
|
+
this.textarea.style.textJustify = 'inter-word';
|
|
23135
|
+
this.textarea.style.wordSpacing = 'normal';
|
|
23136
|
+
|
|
23137
|
+
// Additional CSS properties for better justify matching
|
|
23138
|
+
this.textarea.style.textAlign = 'justify';
|
|
23139
|
+
this.textarea.style.textAlignLast = this.textarea.style.textAlignLast;
|
|
23140
|
+
|
|
23141
|
+
// Try to force better justify behavior
|
|
23142
|
+
this.textarea.style.textJustifyTrim = 'none';
|
|
23143
|
+
this.textarea.style.textAutospace = 'none';
|
|
23144
|
+
console.log(' → Applied justify alignment:', textAlign, 'with last-line:', this.textarea.style.textAlignLast);
|
|
23145
|
+
} catch (error) {
|
|
23146
|
+
console.warn(' → Justify setup failed, falling back to standard alignment:', error);
|
|
23147
|
+
cssTextAlign = textAlign.replace('justify-', '').replace('justify', 'left');
|
|
23148
|
+
}
|
|
23149
|
+
} else {
|
|
23150
|
+
this.textarea.style.textAlignLast = 'auto';
|
|
23151
|
+
this.textarea.style.textJustify = 'auto';
|
|
23152
|
+
this.textarea.style.wordSpacing = 'normal';
|
|
23153
|
+
console.log(' → Applied standard alignment:', cssTextAlign);
|
|
23154
|
+
}
|
|
23155
|
+
this.textarea.style.textAlign = cssTextAlign;
|
|
22115
23156
|
this.textarea.style.color = ((_target$fill = target.fill) === null || _target$fill === void 0 ? void 0 : _target$fill.toString()) || '#000';
|
|
22116
23157
|
this.textarea.style.letterSpacing = `${letterSpacingPx}px`;
|
|
22117
|
-
|
|
23158
|
+
|
|
23159
|
+
// Use the already detected direction from above
|
|
23160
|
+
const fabricDirection = target.direction;
|
|
23161
|
+
|
|
23162
|
+
// Use auto-detected direction for better BiDi support, but respect fabric direction if it makes sense
|
|
23163
|
+
this.textarea.style.direction = autoDetectedDirection || fabricDirection || 'ltr';
|
|
22118
23164
|
this.textarea.style.fontVariant = 'normal';
|
|
22119
23165
|
this.textarea.style.fontStretch = 'normal';
|
|
22120
|
-
this.textarea.style.textRendering = 'optimizeLegibility'
|
|
23166
|
+
this.textarea.style.textRendering = 'auto'; // Changed from 'optimizeLegibility' to match canvas
|
|
22121
23167
|
this.textarea.style.fontKerning = 'normal';
|
|
22122
23168
|
this.textarea.style.fontFeatureSettings = 'normal';
|
|
22123
23169
|
this.textarea.style.fontVariationSettings = 'normal';
|
|
@@ -22128,14 +23174,58 @@ class OverlayEditor {
|
|
|
22128
23174
|
this.textarea.style.overflowWrap = 'break-word';
|
|
22129
23175
|
this.textarea.style.whiteSpace = 'pre-wrap';
|
|
22130
23176
|
this.textarea.style.hyphens = 'none';
|
|
22131
|
-
this.textarea.style.webkitFontSmoothing = 'antialiased';
|
|
22132
|
-
this.textarea.style.mozOsxFontSmoothing = 'grayscale';
|
|
22133
23177
|
|
|
22134
|
-
//
|
|
22135
|
-
|
|
23178
|
+
// DEBUG: Log final CSS properties
|
|
23179
|
+
console.log('🎨 FINAL TEXTAREA CSS:');
|
|
23180
|
+
console.log(' textAlign:', this.textarea.style.textAlign);
|
|
23181
|
+
console.log(' textAlignLast:', this.textarea.style.textAlignLast);
|
|
23182
|
+
console.log(' direction:', this.textarea.style.direction);
|
|
23183
|
+
console.log(' unicodeBidi:', this.textarea.style.unicodeBidi);
|
|
23184
|
+
console.log(' width:', this.textarea.style.width);
|
|
23185
|
+
console.log(' textJustify:', this.textarea.style.textJustify);
|
|
23186
|
+
console.log(' wordSpacing:', this.textarea.style.wordSpacing);
|
|
23187
|
+
console.log(' whiteSpace:', this.textarea.style.whiteSpace);
|
|
23188
|
+
|
|
23189
|
+
// If justify, log Fabric object dimensions for comparison
|
|
23190
|
+
if (textAlign.includes('justify')) {
|
|
23191
|
+
var _calcTextWidth, _ref;
|
|
23192
|
+
console.log('🔧 FABRIC OBJECT JUSTIFY INFO:');
|
|
23193
|
+
console.log(' Fabric width:', target.width);
|
|
23194
|
+
console.log(' Fabric calcTextWidth:', (_calcTextWidth = (_ref = target).calcTextWidth) === null || _calcTextWidth === void 0 ? void 0 : _calcTextWidth.call(_ref));
|
|
23195
|
+
console.log(' Fabric textAlign:', target.textAlign);
|
|
23196
|
+
console.log(' Text lines:', target.textLines);
|
|
23197
|
+
}
|
|
23198
|
+
|
|
23199
|
+
// Debug font properties matching
|
|
23200
|
+
console.log('🔤 FONT PROPERTIES COMPARISON:');
|
|
23201
|
+
console.log(' Fabric fontFamily:', target.fontFamily);
|
|
23202
|
+
console.log(' Fabric fontWeight:', target.fontWeight);
|
|
23203
|
+
console.log(' Fabric fontStyle:', target.fontStyle);
|
|
23204
|
+
console.log(' Fabric fontSize:', target.fontSize);
|
|
23205
|
+
console.log(' → Textarea fontFamily:', this.textarea.style.fontFamily);
|
|
23206
|
+
console.log(' → Textarea fontWeight:', this.textarea.style.fontWeight);
|
|
23207
|
+
console.log(' → Textarea fontStyle:', this.textarea.style.fontStyle);
|
|
23208
|
+
console.log(' → Textarea fontSize:', this.textarea.style.fontSize);
|
|
23209
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
22136
23210
|
|
|
22137
|
-
//
|
|
22138
|
-
|
|
23211
|
+
// Enhanced font rendering to better match fabric.js canvas rendering
|
|
23212
|
+
// Default to auto for more natural rendering
|
|
23213
|
+
this.textarea.style.webkitFontSmoothing = 'auto';
|
|
23214
|
+
this.textarea.style.mozOsxFontSmoothing = 'auto';
|
|
23215
|
+
this.textarea.style.fontSmooth = 'auto';
|
|
23216
|
+
this.textarea.style.textSizeAdjust = 'none';
|
|
23217
|
+
|
|
23218
|
+
// For bold fonts, use subpixel rendering to match canvas thickness better
|
|
23219
|
+
const fontWeight = String(target.fontWeight || 'normal');
|
|
23220
|
+
const isBold = fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 600;
|
|
23221
|
+
if (isBold) {
|
|
23222
|
+
this.textarea.style.webkitFontSmoothing = 'subpixel-antialiased';
|
|
23223
|
+
this.textarea.style.mozOsxFontSmoothing = 'unset';
|
|
23224
|
+
console.log('🔤 Applied enhanced bold rendering for better thickness matching');
|
|
23225
|
+
}
|
|
23226
|
+
console.log('🎨 FONT SMOOTHING APPLIED:');
|
|
23227
|
+
console.log(' webkitFontSmoothing:', this.textarea.style.webkitFontSmoothing);
|
|
23228
|
+
console.log(' mozOsxFontSmoothing:', this.textarea.style.mozOsxFontSmoothing);
|
|
22139
23229
|
|
|
22140
23230
|
// Initial bounds are set correctly by Fabric.js - don't force update here
|
|
22141
23231
|
}
|
|
@@ -22325,6 +23415,11 @@ class OverlayEditor {
|
|
|
22325
23415
|
this.canvas.requestRenderAll();
|
|
22326
23416
|
this.target.setCoords();
|
|
22327
23417
|
this.applyOverlayStyle();
|
|
23418
|
+
|
|
23419
|
+
// Fix character mapping issues after JSON loading for browser-wrapped fonts
|
|
23420
|
+
if (this.target._fixCharacterMappingAfterJsonLoad) {
|
|
23421
|
+
this.target._fixCharacterMappingAfterJsonLoad();
|
|
23422
|
+
}
|
|
22328
23423
|
this.textarea.focus();
|
|
22329
23424
|
this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
|
|
22330
23425
|
|
|
@@ -22370,6 +23465,23 @@ class OverlayEditor {
|
|
|
22370
23465
|
// Handle commit/cancel after restoring visibility
|
|
22371
23466
|
if (commit && !this.isComposing) {
|
|
22372
23467
|
const finalText = this.textarea.value;
|
|
23468
|
+
|
|
23469
|
+
// Auto-detect text direction and update fabric object if needed
|
|
23470
|
+
const detectedDirection = this.firstStrongDir(finalText);
|
|
23471
|
+
const currentDirection = this.target.direction || 'ltr';
|
|
23472
|
+
if (detectedDirection && detectedDirection !== currentDirection) {
|
|
23473
|
+
console.log(`🔄 Overlay Exit: Auto-detected direction change from "${currentDirection}" to "${detectedDirection}"`);
|
|
23474
|
+
console.log(` Text content: "${finalText.substring(0, 50)}..."`);
|
|
23475
|
+
|
|
23476
|
+
// Update the fabric object's direction
|
|
23477
|
+
this.target.set('direction', detectedDirection);
|
|
23478
|
+
|
|
23479
|
+
// Force a re-render to apply the direction change
|
|
23480
|
+
this.canvas.requestRenderAll();
|
|
23481
|
+
console.log(`✅ Fabric object direction updated to: ${detectedDirection}`);
|
|
23482
|
+
} else {
|
|
23483
|
+
console.log(`📝 Overlay Exit: Direction unchanged (${currentDirection}), text: "${finalText.substring(0, 30)}..."`);
|
|
23484
|
+
}
|
|
22373
23485
|
if (this.onCommit) {
|
|
22374
23486
|
this.onCommit(finalText);
|
|
22375
23487
|
}
|
|
@@ -25462,8 +26574,27 @@ class Textbox extends IText {
|
|
|
25462
26574
|
*/
|
|
25463
26575
|
initDimensions() {
|
|
25464
26576
|
if (!this.initialized) {
|
|
26577
|
+
this.initialized = true;
|
|
26578
|
+
}
|
|
26579
|
+
|
|
26580
|
+
// Prevent rapid recalculations during moves
|
|
26581
|
+
if (this._usingBrowserWrapping) {
|
|
26582
|
+
const now = Date.now();
|
|
26583
|
+
const lastCall = this._lastInitDimensionsTime || 0;
|
|
26584
|
+
const isRapidCall = now - lastCall < 100;
|
|
26585
|
+
const isDuringLoading = this._jsonLoading || !this._browserWrapInitialized;
|
|
26586
|
+
if (isRapidCall && !isDuringLoading) {
|
|
26587
|
+
return;
|
|
26588
|
+
}
|
|
26589
|
+
this._lastInitDimensionsTime = now;
|
|
26590
|
+
}
|
|
26591
|
+
|
|
26592
|
+
// Skip if nothing changed
|
|
26593
|
+
const currentState = `${this.text}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
26594
|
+
if (this._lastDimensionState === currentState && this._textLines && this._textLines.length > 0) {
|
|
25465
26595
|
return;
|
|
25466
26596
|
}
|
|
26597
|
+
this._lastDimensionState = currentState;
|
|
25467
26598
|
|
|
25468
26599
|
// Use advanced layout if enabled
|
|
25469
26600
|
if (this.enableAdvancedLayout) {
|
|
@@ -25474,17 +26605,142 @@ class Textbox extends IText {
|
|
|
25474
26605
|
// clear dynamicMinWidth as it will be different after we re-wrap line
|
|
25475
26606
|
this.dynamicMinWidth = 0;
|
|
25476
26607
|
// wrap lines
|
|
25477
|
-
|
|
25478
|
-
|
|
25479
|
-
|
|
26608
|
+
const splitTextResult = this._splitText();
|
|
26609
|
+
this._styleMap = this._generateStyleMap(splitTextResult);
|
|
26610
|
+
|
|
26611
|
+
// For browser wrapping, ensure _textLines is set from browser results
|
|
26612
|
+
if (this._usingBrowserWrapping && splitTextResult && splitTextResult.lines) {
|
|
26613
|
+
this._textLines = splitTextResult.lines.map(line => line.split(''));
|
|
26614
|
+
|
|
26615
|
+
// Store justify measurements and browser height
|
|
26616
|
+
const justifyMeasurements = splitTextResult.justifySpaceMeasurements;
|
|
26617
|
+
if (justifyMeasurements) {
|
|
26618
|
+
this._styleMap.justifySpaceMeasurements = justifyMeasurements;
|
|
26619
|
+
}
|
|
26620
|
+
const actualHeight = splitTextResult.actualBrowserHeight;
|
|
26621
|
+
if (actualHeight) {
|
|
26622
|
+
this._actualBrowserHeight = actualHeight;
|
|
26623
|
+
}
|
|
26624
|
+
}
|
|
26625
|
+
// Don't auto-resize width when using browser wrapping to prevent width increases during moves
|
|
26626
|
+
if (!this._usingBrowserWrapping && this.dynamicMinWidth > this.width) {
|
|
25480
26627
|
this._set('width', this.dynamicMinWidth);
|
|
25481
26628
|
}
|
|
26629
|
+
|
|
26630
|
+
// For browser wrapping fonts (like STV), ensure minimum width for new textboxes
|
|
26631
|
+
// since these fonts can't measure English characters properly
|
|
26632
|
+
if (this._usingBrowserWrapping && this.width < 50) {
|
|
26633
|
+
console.log(`🔤 BROWSER WRAP: Font ${this.fontFamily} has width ${this.width}px, setting to 300px for usability`);
|
|
26634
|
+
this.width = 300;
|
|
26635
|
+
}
|
|
26636
|
+
|
|
26637
|
+
// Mark browser wrapping as initialized when complete
|
|
26638
|
+
if (this._usingBrowserWrapping) {
|
|
26639
|
+
this._browserWrapInitialized = true;
|
|
26640
|
+
}
|
|
25482
26641
|
if (this.textAlign.includes(JUSTIFY)) {
|
|
26642
|
+
// For browser wrapping fonts, apply browser-calculated justify spaces
|
|
26643
|
+
if (this._usingBrowserWrapping) {
|
|
26644
|
+
console.log('🔤 BROWSER WRAP: Applying browser-calculated justify spaces');
|
|
26645
|
+
this._applyBrowserJustifySpaces();
|
|
26646
|
+
return;
|
|
26647
|
+
}
|
|
26648
|
+
|
|
26649
|
+
// Don't apply justify alignment during drag operations to prevent snapping
|
|
26650
|
+
const now = Date.now();
|
|
26651
|
+
const lastDragTime = this._lastInitDimensionsTime || 0;
|
|
26652
|
+
const isDuringDrag = now - lastDragTime < 200; // 200ms window for drag detection
|
|
26653
|
+
|
|
26654
|
+
if (isDuringDrag) {
|
|
26655
|
+
console.log('🔤 Skipping justify during drag operation to prevent snapping');
|
|
26656
|
+
return;
|
|
26657
|
+
}
|
|
26658
|
+
|
|
26659
|
+
// For non-browser-wrapping fonts, use Fabric's justify system
|
|
25483
26660
|
// once text is measured we need to make space fatter to make justified text.
|
|
25484
|
-
|
|
26661
|
+
// Ensure __charBounds exists and fonts are ready before applying justify
|
|
26662
|
+
if (this.__charBounds && this.__charBounds.length > 0) {
|
|
26663
|
+
// Check if font is ready for accurate justify calculations
|
|
26664
|
+
const fontReady = this._isFontReady ? this._isFontReady() : true;
|
|
26665
|
+
if (fontReady) {
|
|
26666
|
+
this.enlargeSpaces();
|
|
26667
|
+
} else {
|
|
26668
|
+
console.warn('⚠️ Textbox: Font not ready for justify, deferring enlargeSpaces');
|
|
26669
|
+
// Defer justify calculation until font is ready
|
|
26670
|
+
this._scheduleJustifyAfterFontLoad();
|
|
26671
|
+
}
|
|
26672
|
+
} else {
|
|
26673
|
+
console.warn('⚠️ Textbox: __charBounds not ready for justify alignment, deferring enlargeSpaces');
|
|
26674
|
+
// Defer the justify calculation until the next frame
|
|
26675
|
+
setTimeout(() => {
|
|
26676
|
+
if (this.__charBounds && this.__charBounds.length > 0 && this.enlargeSpaces) {
|
|
26677
|
+
var _this$canvas;
|
|
26678
|
+
console.log('🔧 Applying deferred Textbox justify alignment');
|
|
26679
|
+
this.enlargeSpaces();
|
|
26680
|
+
(_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
|
|
26681
|
+
}
|
|
26682
|
+
}, 0);
|
|
26683
|
+
}
|
|
26684
|
+
}
|
|
26685
|
+
// Calculate height - use Fabric's calculation for proper text rendering space
|
|
26686
|
+
if (this._usingBrowserWrapping && this._textLines && this._textLines.length > 0) {
|
|
26687
|
+
const actualBrowserHeight = this._actualBrowserHeight;
|
|
26688
|
+
const oldHeight = this.height;
|
|
26689
|
+
// Use Fabric's height calculation since it knows how much space text rendering needs
|
|
26690
|
+
this.height = this.calcTextHeight();
|
|
26691
|
+
|
|
26692
|
+
// Force canvas refresh and control update if height changed significantly
|
|
26693
|
+
if (Math.abs(this.height - oldHeight) > 1) {
|
|
26694
|
+
var _this$canvas2, _this$_textLines;
|
|
26695
|
+
this.setCoords();
|
|
26696
|
+
(_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
|
|
26697
|
+
|
|
26698
|
+
// DEBUG: Log exact positioning details
|
|
26699
|
+
console.log(`🎯 POSITIONING DEBUG:`);
|
|
26700
|
+
console.log(` Textbox height: ${this.height}px`);
|
|
26701
|
+
console.log(` Textbox top: ${this.top}px`);
|
|
26702
|
+
console.log(` Textbox left: ${this.left}px`);
|
|
26703
|
+
console.log(` Text lines: ${((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0}`);
|
|
26704
|
+
console.log(` Font size: ${this.fontSize}px`);
|
|
26705
|
+
console.log(` Line height: ${this.lineHeight || 1.16}`);
|
|
26706
|
+
console.log(` Calculated line height: ${this.fontSize * (this.lineHeight || 1.16)}px`);
|
|
26707
|
+
console.log(` _getTopOffset(): ${this._getTopOffset()}px`);
|
|
26708
|
+
console.log(` calcTextHeight(): ${this.calcTextHeight()}px`);
|
|
26709
|
+
console.log(` Browser height: ${actualBrowserHeight}px`);
|
|
26710
|
+
console.log(` Height difference: ${this.height - this.calcTextHeight()}px`);
|
|
26711
|
+
}
|
|
26712
|
+
} else {
|
|
26713
|
+
this.height = this.calcTextHeight();
|
|
26714
|
+
}
|
|
26715
|
+
}
|
|
26716
|
+
|
|
26717
|
+
/**
|
|
26718
|
+
* Schedule justify calculation after font loads (Textbox-specific)
|
|
26719
|
+
* @private
|
|
26720
|
+
*/
|
|
26721
|
+
_scheduleJustifyAfterFontLoad() {
|
|
26722
|
+
if (typeof document === 'undefined' || !('fonts' in document)) {
|
|
26723
|
+
return;
|
|
26724
|
+
}
|
|
26725
|
+
|
|
26726
|
+
// Only schedule if not already waiting
|
|
26727
|
+
if (this._fontJustifyScheduled) {
|
|
26728
|
+
return;
|
|
25485
26729
|
}
|
|
25486
|
-
|
|
25487
|
-
|
|
26730
|
+
this._fontJustifyScheduled = true;
|
|
26731
|
+
const fontSpec = `${this.fontSize}px ${this.fontFamily}`;
|
|
26732
|
+
document.fonts.load(fontSpec).then(() => {
|
|
26733
|
+
var _this$canvas3;
|
|
26734
|
+
this._fontJustifyScheduled = false;
|
|
26735
|
+
console.log('🔧 Textbox: Font loaded, applying justify alignment');
|
|
26736
|
+
|
|
26737
|
+
// Re-run initDimensions to ensure proper justify calculation
|
|
26738
|
+
this.initDimensions();
|
|
26739
|
+
(_this$canvas3 = this.canvas) === null || _this$canvas3 === void 0 || _this$canvas3.requestRenderAll();
|
|
26740
|
+
}).catch(() => {
|
|
26741
|
+
this._fontJustifyScheduled = false;
|
|
26742
|
+
console.warn('⚠️ Textbox: Font loading failed, justify may be incorrect');
|
|
26743
|
+
});
|
|
25488
26744
|
}
|
|
25489
26745
|
|
|
25490
26746
|
/**
|
|
@@ -25851,19 +27107,33 @@ class Textbox extends IText {
|
|
|
25851
27107
|
width: wordWidth
|
|
25852
27108
|
} = data[i];
|
|
25853
27109
|
offset += word.length;
|
|
25854
|
-
|
|
25855
|
-
if
|
|
27110
|
+
|
|
27111
|
+
// Predictive wrapping: check if adding this word would exceed the width
|
|
27112
|
+
const potentialLineWidth = lineWidth + infixWidth + wordWidth - additionalSpace;
|
|
27113
|
+
// Use exact width to match overlay editor behavior
|
|
27114
|
+
const conservativeMaxWidth = maxWidth; // No artificial buffer
|
|
27115
|
+
|
|
27116
|
+
// Debug logging for wrapping decisions
|
|
27117
|
+
const currentLineText = line.join('');
|
|
27118
|
+
console.log(`🔧 FABRIC WRAP CHECK: "${data[i].word}" -> potential: ${potentialLineWidth.toFixed(1)}px vs limit: ${conservativeMaxWidth.toFixed(1)}px`);
|
|
27119
|
+
if (potentialLineWidth > conservativeMaxWidth && !lineJustStarted) {
|
|
27120
|
+
// This word would exceed the width, wrap before adding it
|
|
27121
|
+
console.log(`🔧 FABRIC WRAP! Line: "${currentLineText}" (${lineWidth.toFixed(1)}px)`);
|
|
25856
27122
|
graphemeLines.push(line);
|
|
25857
27123
|
line = [];
|
|
25858
|
-
lineWidth = wordWidth;
|
|
27124
|
+
lineWidth = wordWidth; // Start new line with just this word
|
|
25859
27125
|
lineJustStarted = true;
|
|
25860
27126
|
} else {
|
|
25861
|
-
|
|
27127
|
+
// Word fits, add it to current line
|
|
27128
|
+
lineWidth = potentialLineWidth + additionalSpace;
|
|
25862
27129
|
}
|
|
25863
27130
|
if (!lineJustStarted && !splitByGrapheme) {
|
|
25864
27131
|
line.push(infix);
|
|
25865
27132
|
}
|
|
25866
27133
|
line = line.concat(word);
|
|
27134
|
+
|
|
27135
|
+
// Debug: show current line after adding word
|
|
27136
|
+
console.log(`🔧 FABRIC AFTER ADD: Line now: "${line.join('')}" (${line.length} chars)`);
|
|
25867
27137
|
infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
|
|
25868
27138
|
offset++;
|
|
25869
27139
|
lineJustStarted = false;
|
|
@@ -25873,9 +27143,19 @@ class Textbox extends IText {
|
|
|
25873
27143
|
// TODO: this code is probably not necessary anymore.
|
|
25874
27144
|
// it can be moved out of this function since largestWordWidth is now
|
|
25875
27145
|
// known in advance
|
|
25876
|
-
|
|
27146
|
+
// Don't modify dynamicMinWidth when using browser wrapping to prevent width increases
|
|
27147
|
+
if (!this._usingBrowserWrapping && largestWordWidth + reservedSpace > this.dynamicMinWidth) {
|
|
27148
|
+
console.log(`🔧 FABRIC updating dynamicMinWidth: ${this.dynamicMinWidth} -> ${largestWordWidth - additionalSpace + reservedSpace}`);
|
|
25877
27149
|
this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
|
|
27150
|
+
} else if (this._usingBrowserWrapping) {
|
|
27151
|
+
console.log(`🔤 BROWSER WRAP: Skipping dynamicMinWidth update to prevent width increase`);
|
|
25878
27152
|
}
|
|
27153
|
+
|
|
27154
|
+
// Debug: show final wrapped lines
|
|
27155
|
+
console.log(`🔧 FABRIC FINAL LINES: ${graphemeLines.length} lines`);
|
|
27156
|
+
graphemeLines.forEach((line, i) => {
|
|
27157
|
+
console.log(` Line ${i + 1}: "${line.join('')}" (${line.length} chars)`);
|
|
27158
|
+
});
|
|
25879
27159
|
return graphemeLines;
|
|
25880
27160
|
}
|
|
25881
27161
|
|
|
@@ -25919,6 +27199,260 @@ class Textbox extends IText {
|
|
|
25919
27199
|
* @override
|
|
25920
27200
|
*/
|
|
25921
27201
|
_splitTextIntoLines(text) {
|
|
27202
|
+
// Check if we need browser wrapping using smart font detection
|
|
27203
|
+
const needsBrowserWrapping = this.fontFamily && fontLacksEnglishGlyphsCached(this.fontFamily);
|
|
27204
|
+
if (needsBrowserWrapping) {
|
|
27205
|
+
// Cache key based on text content, width, font properties, AND text alignment
|
|
27206
|
+
const textHash = text.length + text.slice(0, 50); // Include text content in cache key
|
|
27207
|
+
const cacheKey = `${textHash}|${this.width}|${this.fontSize}|${this.fontFamily}|${this.textAlign}`;
|
|
27208
|
+
|
|
27209
|
+
// Check if we have a cached result and nothing has changed
|
|
27210
|
+
if (this._browserWrapCache && this._browserWrapCache.key === cacheKey) {
|
|
27211
|
+
const cachedResult = this._browserWrapCache.result;
|
|
27212
|
+
|
|
27213
|
+
// For justify alignment, ensure we have the measurements
|
|
27214
|
+
if (this.textAlign.includes('justify') && !cachedResult.justifySpaceMeasurements) ; else {
|
|
27215
|
+
return cachedResult;
|
|
27216
|
+
}
|
|
27217
|
+
}
|
|
27218
|
+
const result = this._splitTextIntoLinesWithBrowser(text);
|
|
27219
|
+
|
|
27220
|
+
// Cache the result
|
|
27221
|
+
this._browserWrapCache = {
|
|
27222
|
+
key: cacheKey,
|
|
27223
|
+
result
|
|
27224
|
+
};
|
|
27225
|
+
|
|
27226
|
+
// Mark that we used browser wrapping to prevent dynamicMinWidth modifications
|
|
27227
|
+
this._usingBrowserWrapping = true;
|
|
27228
|
+
return result;
|
|
27229
|
+
}
|
|
27230
|
+
|
|
27231
|
+
// Clear the browser wrapping flag when using regular wrapping
|
|
27232
|
+
this._usingBrowserWrapping = false;
|
|
27233
|
+
|
|
27234
|
+
// Default Fabric wrapping for other fonts
|
|
27235
|
+
const newText = super._splitTextIntoLines(text),
|
|
27236
|
+
graphemeLines = this._wrapText(newText.lines, this.width),
|
|
27237
|
+
lines = new Array(graphemeLines.length);
|
|
27238
|
+
for (let i = 0; i < graphemeLines.length; i++) {
|
|
27239
|
+
lines[i] = graphemeLines[i].join('');
|
|
27240
|
+
}
|
|
27241
|
+
newText.lines = lines;
|
|
27242
|
+
newText.graphemeLines = graphemeLines;
|
|
27243
|
+
return newText;
|
|
27244
|
+
}
|
|
27245
|
+
|
|
27246
|
+
/**
|
|
27247
|
+
* Use browser's native text wrapping for accurate handling of fonts without English glyphs
|
|
27248
|
+
* @private
|
|
27249
|
+
*/
|
|
27250
|
+
_splitTextIntoLinesWithBrowser(text) {
|
|
27251
|
+
if (typeof document === 'undefined') {
|
|
27252
|
+
// Fallback to regular wrapping in Node.js
|
|
27253
|
+
return this._splitTextIntoLinesDefault(text);
|
|
27254
|
+
}
|
|
27255
|
+
|
|
27256
|
+
// Create a hidden element that mimics the overlay editor
|
|
27257
|
+
const testElement = document.createElement('div');
|
|
27258
|
+
testElement.style.position = 'absolute';
|
|
27259
|
+
testElement.style.left = '-9999px';
|
|
27260
|
+
testElement.style.visibility = 'hidden';
|
|
27261
|
+
testElement.style.fontSize = `${this.fontSize}px`;
|
|
27262
|
+
testElement.style.fontFamily = `"${this.fontFamily}"`;
|
|
27263
|
+
testElement.style.fontWeight = String(this.fontWeight || 'normal');
|
|
27264
|
+
testElement.style.fontStyle = String(this.fontStyle || 'normal');
|
|
27265
|
+
testElement.style.lineHeight = String(this.lineHeight || 1.16);
|
|
27266
|
+
testElement.style.width = `${this.width}px`;
|
|
27267
|
+
testElement.style.direction = this.direction || 'ltr';
|
|
27268
|
+
testElement.style.whiteSpace = 'pre-wrap';
|
|
27269
|
+
testElement.style.wordBreak = 'normal';
|
|
27270
|
+
testElement.style.overflowWrap = 'break-word';
|
|
27271
|
+
|
|
27272
|
+
// Set browser-native text alignment (including justify)
|
|
27273
|
+
if (this.textAlign.includes('justify')) {
|
|
27274
|
+
testElement.style.textAlign = 'justify';
|
|
27275
|
+
testElement.style.textAlignLast = 'auto'; // Let browser decide last line alignment
|
|
27276
|
+
} else {
|
|
27277
|
+
testElement.style.textAlign = this.textAlign;
|
|
27278
|
+
}
|
|
27279
|
+
testElement.textContent = text;
|
|
27280
|
+
document.body.appendChild(testElement);
|
|
27281
|
+
|
|
27282
|
+
// Get the browser's natural line breaks
|
|
27283
|
+
const range = document.createRange();
|
|
27284
|
+
const lines = [];
|
|
27285
|
+
const graphemeLines = [];
|
|
27286
|
+
try {
|
|
27287
|
+
// Simple approach: split by measuring character positions
|
|
27288
|
+
const textNode = testElement.firstChild;
|
|
27289
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
27290
|
+
let currentLineStart = 0;
|
|
27291
|
+
const textLength = text.length;
|
|
27292
|
+
let previousBottom = 0;
|
|
27293
|
+
for (let i = 0; i <= textLength; i++) {
|
|
27294
|
+
range.setStart(textNode, currentLineStart);
|
|
27295
|
+
range.setEnd(textNode, i);
|
|
27296
|
+
const rect = range.getBoundingClientRect();
|
|
27297
|
+
if (i > currentLineStart && (rect.bottom > previousBottom + 5 || i === textLength)) {
|
|
27298
|
+
// New line detected or end of text
|
|
27299
|
+
const lineEnd = i === textLength ? i : i - 1;
|
|
27300
|
+
const lineText = text.substring(currentLineStart, lineEnd).trim();
|
|
27301
|
+
if (lineText) {
|
|
27302
|
+
lines.push(lineText);
|
|
27303
|
+
// Convert to graphemes for compatibility
|
|
27304
|
+
const graphemeLine = lineText.split('');
|
|
27305
|
+
graphemeLines.push(graphemeLine);
|
|
27306
|
+
}
|
|
27307
|
+
currentLineStart = lineEnd;
|
|
27308
|
+
previousBottom = rect.bottom;
|
|
27309
|
+
}
|
|
27310
|
+
}
|
|
27311
|
+
}
|
|
27312
|
+
} catch (error) {
|
|
27313
|
+
console.warn('Browser wrapping failed, using fallback:', error);
|
|
27314
|
+
document.body.removeChild(testElement);
|
|
27315
|
+
return this._splitTextIntoLinesDefault(text);
|
|
27316
|
+
}
|
|
27317
|
+
|
|
27318
|
+
// Extract actual browser height BEFORE removing element
|
|
27319
|
+
const actualBrowserHeight = testElement.scrollHeight;
|
|
27320
|
+
const offsetHeight = testElement.offsetHeight;
|
|
27321
|
+
const clientHeight = testElement.clientHeight;
|
|
27322
|
+
const boundingRect = testElement.getBoundingClientRect();
|
|
27323
|
+
console.log(`🔤 Browser element measurements:`);
|
|
27324
|
+
console.log(` scrollHeight: ${actualBrowserHeight}px (content + padding + hidden overflow)`);
|
|
27325
|
+
console.log(` offsetHeight: ${offsetHeight}px (content + padding + border)`);
|
|
27326
|
+
console.log(` clientHeight: ${clientHeight}px (content + padding, no border/scrollbar)`);
|
|
27327
|
+
console.log(` boundingRect.height: ${boundingRect.height}px (actual rendered height)`);
|
|
27328
|
+
console.log(` Font size: ${this.fontSize}px, Line height: ${this.lineHeight || 1.16}, Lines: ${lines.length}`);
|
|
27329
|
+
|
|
27330
|
+
// For justify alignment, extract space measurements from browser BEFORE removing element
|
|
27331
|
+
let justifySpaceMeasurements = null;
|
|
27332
|
+
if (this.textAlign.includes('justify')) {
|
|
27333
|
+
justifySpaceMeasurements = this._extractJustifySpaceMeasurements(testElement, lines);
|
|
27334
|
+
}
|
|
27335
|
+
document.body.removeChild(testElement);
|
|
27336
|
+
console.log(`🔤 Browser wrapping result: ${lines.length} lines`);
|
|
27337
|
+
|
|
27338
|
+
// Try different height measurements to find the most accurate
|
|
27339
|
+
let bestHeight = actualBrowserHeight;
|
|
27340
|
+
|
|
27341
|
+
// If scrollHeight and offsetHeight differ significantly, investigate
|
|
27342
|
+
if (Math.abs(actualBrowserHeight - offsetHeight) > 2) {
|
|
27343
|
+
console.log(`🔤 Height discrepancy detected: scrollHeight=${actualBrowserHeight}px vs offsetHeight=${offsetHeight}px`);
|
|
27344
|
+
}
|
|
27345
|
+
|
|
27346
|
+
// Consider using boundingRect height if it's larger (sometimes more accurate for visible content)
|
|
27347
|
+
if (boundingRect.height > bestHeight) {
|
|
27348
|
+
console.log(`🔤 Using boundingRect height (${boundingRect.height}px) instead of scrollHeight (${bestHeight}px)`);
|
|
27349
|
+
bestHeight = boundingRect.height;
|
|
27350
|
+
}
|
|
27351
|
+
|
|
27352
|
+
// Font-specific height adjustments for accurate bounding box
|
|
27353
|
+
let adjustedHeight = bestHeight;
|
|
27354
|
+
|
|
27355
|
+
// Fonts without English glyphs need additional height buffer due to different font metrics
|
|
27356
|
+
const lacksEnglishGlyphs = fontLacksEnglishGlyphsCached(this.fontFamily);
|
|
27357
|
+
if (lacksEnglishGlyphs) {
|
|
27358
|
+
const glyphBuffer = this.fontSize * 0.25; // 25% of font size for non-English fonts
|
|
27359
|
+
adjustedHeight = bestHeight + glyphBuffer;
|
|
27360
|
+
console.log(`🔤 Non-English font detected (${this.fontFamily}): Adding ${glyphBuffer}px buffer (${bestHeight}px + ${glyphBuffer}px = ${adjustedHeight}px)`);
|
|
27361
|
+
} else {
|
|
27362
|
+
console.log(`🔤 Standard font (${this.fontFamily}): Using browser height directly (${bestHeight}px)`);
|
|
27363
|
+
}
|
|
27364
|
+
return {
|
|
27365
|
+
_unwrappedLines: [text.split('')],
|
|
27366
|
+
lines: lines,
|
|
27367
|
+
graphemeText: text.split(''),
|
|
27368
|
+
graphemeLines: graphemeLines,
|
|
27369
|
+
justifySpaceMeasurements: justifySpaceMeasurements,
|
|
27370
|
+
actualBrowserHeight: adjustedHeight
|
|
27371
|
+
};
|
|
27372
|
+
}
|
|
27373
|
+
|
|
27374
|
+
/**
|
|
27375
|
+
* Extract justify space measurements from browser
|
|
27376
|
+
* @private
|
|
27377
|
+
*/
|
|
27378
|
+
_extractJustifySpaceMeasurements(element, lines) {
|
|
27379
|
+
console.log(`🔤 Extracting browser justify space measurements for ${lines.length} lines`);
|
|
27380
|
+
|
|
27381
|
+
// For now, we'll use a simplified approach:
|
|
27382
|
+
// Apply uniform space expansion to match the line width
|
|
27383
|
+
const spaceWidths = [];
|
|
27384
|
+
lines.forEach((line, lineIndex) => {
|
|
27385
|
+
const lineSpaces = [];
|
|
27386
|
+
const spaceCount = (line.match(/\s/g) || []).length;
|
|
27387
|
+
if (spaceCount > 0 && lineIndex < lines.length - 1) {
|
|
27388
|
+
// Don't justify last line
|
|
27389
|
+
// Calculate how much space expansion is needed
|
|
27390
|
+
const normalSpaceWidth = 6.4; // Default space width for STV font
|
|
27391
|
+
const lineWidth = this.width;
|
|
27392
|
+
|
|
27393
|
+
// Estimate natural line width
|
|
27394
|
+
const charCount = line.length - spaceCount;
|
|
27395
|
+
const avgCharWidth = 12; // Approximate for STV font
|
|
27396
|
+
|
|
27397
|
+
// Calculate expanded space width
|
|
27398
|
+
const remainingSpace = lineWidth - charCount * avgCharWidth;
|
|
27399
|
+
const expandedSpaceWidth = remainingSpace / spaceCount;
|
|
27400
|
+
console.log(`🔤 Line ${lineIndex}: ${spaceCount} spaces, natural: ${normalSpaceWidth}px -> justified: ${expandedSpaceWidth.toFixed(1)}px`);
|
|
27401
|
+
|
|
27402
|
+
// Fill array with expanded space widths for this line
|
|
27403
|
+
for (let i = 0; i < spaceCount; i++) {
|
|
27404
|
+
lineSpaces.push(expandedSpaceWidth);
|
|
27405
|
+
}
|
|
27406
|
+
}
|
|
27407
|
+
spaceWidths.push(lineSpaces);
|
|
27408
|
+
});
|
|
27409
|
+
return spaceWidths;
|
|
27410
|
+
}
|
|
27411
|
+
|
|
27412
|
+
/**
|
|
27413
|
+
* Apply browser-calculated justify space measurements
|
|
27414
|
+
* @private
|
|
27415
|
+
*/
|
|
27416
|
+
_applyBrowserJustifySpaces() {
|
|
27417
|
+
if (!this._textLines || !this.__charBounds) {
|
|
27418
|
+
console.warn('🔤 BROWSER JUSTIFY: _textLines or __charBounds not ready');
|
|
27419
|
+
return;
|
|
27420
|
+
}
|
|
27421
|
+
|
|
27422
|
+
// Get space measurements from browser wrapping result
|
|
27423
|
+
const styleMap = this._styleMap;
|
|
27424
|
+
if (!styleMap || !styleMap.justifySpaceMeasurements) {
|
|
27425
|
+
console.warn('🔤 BROWSER JUSTIFY: No justify space measurements available');
|
|
27426
|
+
return;
|
|
27427
|
+
}
|
|
27428
|
+
const spaceWidths = styleMap.justifySpaceMeasurements;
|
|
27429
|
+
console.log('🔤 BROWSER JUSTIFY: Applying space measurements to __charBounds');
|
|
27430
|
+
|
|
27431
|
+
// Apply space widths to character bounds
|
|
27432
|
+
this._textLines.forEach((line, lineIndex) => {
|
|
27433
|
+
if (!this.__charBounds || !this.__charBounds[lineIndex] || !spaceWidths[lineIndex]) return;
|
|
27434
|
+
const lineBounds = this.__charBounds[lineIndex];
|
|
27435
|
+
const lineSpaceWidths = spaceWidths[lineIndex];
|
|
27436
|
+
let spaceIndex = 0;
|
|
27437
|
+
for (let charIndex = 0; charIndex < line.length; charIndex++) {
|
|
27438
|
+
if (/\s/.test(line[charIndex]) && spaceIndex < lineSpaceWidths.length) {
|
|
27439
|
+
const expandedWidth = lineSpaceWidths[spaceIndex];
|
|
27440
|
+
if (lineBounds[charIndex]) {
|
|
27441
|
+
const oldWidth = lineBounds[charIndex].width;
|
|
27442
|
+
lineBounds[charIndex].width = expandedWidth;
|
|
27443
|
+
console.log(`🔤 Line ${lineIndex} space ${spaceIndex}: ${oldWidth.toFixed(1)}px -> ${expandedWidth.toFixed(1)}px`);
|
|
27444
|
+
}
|
|
27445
|
+
spaceIndex++;
|
|
27446
|
+
}
|
|
27447
|
+
}
|
|
27448
|
+
});
|
|
27449
|
+
}
|
|
27450
|
+
|
|
27451
|
+
/**
|
|
27452
|
+
* Fallback to default Fabric wrapping
|
|
27453
|
+
* @private
|
|
27454
|
+
*/
|
|
27455
|
+
_splitTextIntoLinesDefault(text) {
|
|
25922
27456
|
const newText = super._splitTextIntoLines(text),
|
|
25923
27457
|
graphemeLines = this._wrapText(newText.lines, this.width),
|
|
25924
27458
|
lines = new Array(graphemeLines.length);
|
|
@@ -25953,37 +27487,24 @@ class Textbox extends IText {
|
|
|
25953
27487
|
* @private
|
|
25954
27488
|
*/
|
|
25955
27489
|
initializeEventListeners() {
|
|
25956
|
-
var _this$
|
|
27490
|
+
var _this$canvas4;
|
|
25957
27491
|
// Track which side is being used for resize to handle position compensation
|
|
25958
27492
|
let resizeOrigin = null;
|
|
25959
27493
|
|
|
25960
27494
|
// Detect resize origin during resizing
|
|
25961
27495
|
this.on('resizing', e => {
|
|
25962
27496
|
// Check transform origin to determine which side is being resized
|
|
25963
|
-
console.log('🔍 Resize event data:', e);
|
|
25964
27497
|
if (e.transform) {
|
|
25965
27498
|
const {
|
|
25966
|
-
originX
|
|
25967
|
-
originY
|
|
27499
|
+
originX
|
|
25968
27500
|
} = e.transform;
|
|
25969
|
-
console.log('🔍 Transform origins:', {
|
|
25970
|
-
originX,
|
|
25971
|
-
originY
|
|
25972
|
-
});
|
|
25973
27501
|
// originX tells us which side is the anchor - opposite side is being dragged
|
|
25974
27502
|
resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
|
|
25975
|
-
console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
|
|
25976
27503
|
} else if (e.originX) {
|
|
25977
27504
|
const {
|
|
25978
|
-
originX
|
|
25979
|
-
originY
|
|
27505
|
+
originX
|
|
25980
27506
|
} = e;
|
|
25981
|
-
console.log('🔍 Event origins:', {
|
|
25982
|
-
originX,
|
|
25983
|
-
originY
|
|
25984
|
-
});
|
|
25985
27507
|
resizeOrigin = originX === 'right' ? 'left' : originX === 'left' ? 'right' : null;
|
|
25986
|
-
console.log('🎯 Setting resizeOrigin to:', resizeOrigin);
|
|
25987
27508
|
}
|
|
25988
27509
|
});
|
|
25989
27510
|
|
|
@@ -25991,19 +27512,15 @@ class Textbox extends IText {
|
|
|
25991
27512
|
// Use 'modified' event which fires after user releases the mouse
|
|
25992
27513
|
this.on('modified', () => {
|
|
25993
27514
|
const currentResizeOrigin = resizeOrigin; // Capture the value before reset
|
|
25994
|
-
console.log('✅ Modified event fired - resize complete, triggering safety snap', {
|
|
25995
|
-
resizeOrigin: currentResizeOrigin
|
|
25996
|
-
});
|
|
25997
27515
|
// Small delay to ensure text layout is updated
|
|
25998
27516
|
setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
|
|
25999
27517
|
resizeOrigin = null; // Reset after capturing
|
|
26000
27518
|
});
|
|
26001
27519
|
|
|
26002
27520
|
// Also listen to canvas-level modified event as backup
|
|
26003
|
-
(_this$
|
|
27521
|
+
(_this$canvas4 = this.canvas) === null || _this$canvas4 === void 0 || _this$canvas4.on('object:modified', e => {
|
|
26004
27522
|
if (e.target === this) {
|
|
26005
27523
|
const currentResizeOrigin = resizeOrigin; // Capture the value before reset
|
|
26006
|
-
console.log('✅ Canvas object:modified fired for this textbox');
|
|
26007
27524
|
setTimeout(() => this.safetySnapWidth(currentResizeOrigin), 10);
|
|
26008
27525
|
resizeOrigin = null; // Reset after capturing
|
|
26009
27526
|
}
|
|
@@ -26018,38 +27535,17 @@ class Textbox extends IText {
|
|
|
26018
27535
|
* @param resizeOrigin - Which side was used for resizing ('left' or 'right')
|
|
26019
27536
|
*/
|
|
26020
27537
|
safetySnapWidth(resizeOrigin) {
|
|
26021
|
-
var _this$_textLines;
|
|
26022
|
-
console.log('🔍 safetySnapWidth called', {
|
|
26023
|
-
isWrapping: this.isWrapping,
|
|
26024
|
-
hasTextLines: !!this._textLines,
|
|
26025
|
-
lineCount: ((_this$_textLines = this._textLines) === null || _this$_textLines === void 0 ? void 0 : _this$_textLines.length) || 0,
|
|
26026
|
-
currentWidth: this.width,
|
|
26027
|
-
type: this.type,
|
|
26028
|
-
text: this.text
|
|
26029
|
-
});
|
|
26030
|
-
|
|
26031
27538
|
// For Textbox objects, we always want to check for clipping regardless of isWrapping flag
|
|
26032
27539
|
if (!this._textLines || this.type.toLowerCase() !== 'textbox' || this._textLines.length === 0) {
|
|
26033
|
-
var _this$_textLines2;
|
|
26034
|
-
console.log('❌ Early return - missing requirements', {
|
|
26035
|
-
hasTextLines: !!this._textLines,
|
|
26036
|
-
typeMatch: this.type.toLowerCase() === 'textbox',
|
|
26037
|
-
actualType: this.type,
|
|
26038
|
-
hasLines: ((_this$_textLines2 = this._textLines) === null || _this$_textLines2 === void 0 ? void 0 : _this$_textLines2.length) > 0
|
|
26039
|
-
});
|
|
26040
27540
|
return;
|
|
26041
27541
|
}
|
|
26042
27542
|
const lineCount = this._textLines.length;
|
|
26043
27543
|
if (lineCount === 0) return;
|
|
26044
|
-
|
|
26045
|
-
// Check all lines, not just the last one
|
|
26046
|
-
let maxActualLineWidth = 0; // Actual measured width without buffers
|
|
26047
27544
|
let maxRequiredWidth = 0; // Width including RTL buffer
|
|
26048
27545
|
|
|
26049
27546
|
for (let i = 0; i < lineCount; i++) {
|
|
26050
27547
|
const lineText = this._textLines[i].join(''); // Convert grapheme array to string
|
|
26051
27548
|
const lineWidth = this.getLineWidth(i);
|
|
26052
|
-
maxActualLineWidth = Math.max(maxActualLineWidth, lineWidth);
|
|
26053
27549
|
|
|
26054
27550
|
// RTL detection - regex for Arabic, Hebrew, and other RTL characters
|
|
26055
27551
|
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
|
@@ -26066,14 +27562,9 @@ class Textbox extends IText {
|
|
|
26066
27562
|
const safetyThreshold = 2; // px - very subtle trigger
|
|
26067
27563
|
|
|
26068
27564
|
if (maxRequiredWidth > this.width - safetyThreshold) {
|
|
26069
|
-
var _this$
|
|
27565
|
+
var _this$canvas5;
|
|
26070
27566
|
// Set width to exactly what's needed + minimal safety margin
|
|
26071
27567
|
const newWidth = maxRequiredWidth + 1; // Add just 1px safety margin
|
|
26072
|
-
console.log(`Safety snap: ${this.width.toFixed(0)}px -> ${newWidth.toFixed(0)}px`, {
|
|
26073
|
-
maxActualLineWidth: maxActualLineWidth.toFixed(1),
|
|
26074
|
-
maxRequiredWidth: maxRequiredWidth.toFixed(1),
|
|
26075
|
-
difference: (newWidth - this.width).toFixed(1)
|
|
26076
|
-
});
|
|
26077
27568
|
|
|
26078
27569
|
// Store original position before width change
|
|
26079
27570
|
const originalLeft = this.left;
|
|
@@ -26089,19 +27580,12 @@ class Textbox extends IText {
|
|
|
26089
27580
|
// Only compensate position when resizing from left handle
|
|
26090
27581
|
// Right handle resize doesn't shift the text position
|
|
26091
27582
|
if (resizeOrigin === 'left') {
|
|
26092
|
-
console.log('🔧 Compensating for left-side resize', {
|
|
26093
|
-
originalLeft,
|
|
26094
|
-
widthIncrease,
|
|
26095
|
-
newLeft: originalLeft - widthIncrease
|
|
26096
|
-
});
|
|
26097
27583
|
// When resizing from left, the expansion pushes text right
|
|
26098
27584
|
// Compensate by moving the textbox left by the width increase
|
|
26099
27585
|
this.set({
|
|
26100
27586
|
'left': originalLeft - widthIncrease,
|
|
26101
27587
|
'top': originalTop
|
|
26102
27588
|
});
|
|
26103
|
-
} else {
|
|
26104
|
-
console.log('✅ Right-side resize, no compensation needed');
|
|
26105
27589
|
}
|
|
26106
27590
|
this.setCoords();
|
|
26107
27591
|
|
|
@@ -26111,7 +27595,88 @@ class Textbox extends IText {
|
|
|
26111
27595
|
this.__overlayEditor.refresh();
|
|
26112
27596
|
}, 0);
|
|
26113
27597
|
}
|
|
26114
|
-
(_this$
|
|
27598
|
+
(_this$canvas5 = this.canvas) === null || _this$canvas5 === void 0 || _this$canvas5.requestRenderAll();
|
|
27599
|
+
}
|
|
27600
|
+
}
|
|
27601
|
+
|
|
27602
|
+
/**
|
|
27603
|
+
* Fix character selection mismatch after JSON loading for browser-wrapped fonts
|
|
27604
|
+
* @private
|
|
27605
|
+
*/
|
|
27606
|
+
_fixCharacterMappingAfterJsonLoad() {
|
|
27607
|
+
if (this._usingBrowserWrapping) {
|
|
27608
|
+
// Clear all cached states to force fresh text layout calculation
|
|
27609
|
+
this._browserWrapCache = null;
|
|
27610
|
+
this._lastDimensionState = null;
|
|
27611
|
+
|
|
27612
|
+
// Force complete re-initialization
|
|
27613
|
+
this.initDimensions();
|
|
27614
|
+
this._forceClearCache = true;
|
|
27615
|
+
|
|
27616
|
+
// Ensure canvas refresh
|
|
27617
|
+
this.setCoords();
|
|
27618
|
+
if (this.canvas) {
|
|
27619
|
+
this.canvas.requestRenderAll();
|
|
27620
|
+
}
|
|
27621
|
+
}
|
|
27622
|
+
}
|
|
27623
|
+
|
|
27624
|
+
/**
|
|
27625
|
+
* Force complete textbox re-initialization (useful after JSON loading)
|
|
27626
|
+
* Overrides Text version with Textbox-specific logic
|
|
27627
|
+
*/
|
|
27628
|
+
forceTextReinitialization() {
|
|
27629
|
+
console.log('🔄 Force reinitializing Textbox object');
|
|
27630
|
+
|
|
27631
|
+
// CRITICAL: Ensure textbox is marked as initialized
|
|
27632
|
+
this.initialized = true;
|
|
27633
|
+
|
|
27634
|
+
// Clear all caches and force dirty state
|
|
27635
|
+
this._clearCache();
|
|
27636
|
+
this.dirty = true;
|
|
27637
|
+
this.dynamicMinWidth = 0;
|
|
27638
|
+
|
|
27639
|
+
// Force isEditing false to ensure clean state
|
|
27640
|
+
this.isEditing = false;
|
|
27641
|
+
console.log(' → Set initialized=true, dirty=true, cleared caches');
|
|
27642
|
+
|
|
27643
|
+
// Re-initialize dimensions (this will handle justify properly)
|
|
27644
|
+
this.initDimensions();
|
|
27645
|
+
|
|
27646
|
+
// Double-check that justify was applied by checking space widths
|
|
27647
|
+
if (this.textAlign.includes('justify') && this.__charBounds) {
|
|
27648
|
+
setTimeout(() => {
|
|
27649
|
+
var _this$canvas6;
|
|
27650
|
+
// Verify justify was applied by checking if space widths vary
|
|
27651
|
+
let hasVariableSpaces = false;
|
|
27652
|
+
this.__charBounds.forEach((lineBounds, i) => {
|
|
27653
|
+
if (lineBounds && this._textLines && this._textLines[i]) {
|
|
27654
|
+
const spaces = lineBounds.filter((bound, j) => /\s/.test(this._textLines[i][j]));
|
|
27655
|
+
if (spaces.length > 1) {
|
|
27656
|
+
const firstSpaceWidth = spaces[0].width;
|
|
27657
|
+
hasVariableSpaces = spaces.some(space => Math.abs(space.width - firstSpaceWidth) > 0.1);
|
|
27658
|
+
}
|
|
27659
|
+
}
|
|
27660
|
+
});
|
|
27661
|
+
if (!hasVariableSpaces && this.__charBounds.length > 0) {
|
|
27662
|
+
console.warn(' ⚠️ Justify spaces still uniform - forcing enlargeSpaces again');
|
|
27663
|
+
if (this.enlargeSpaces) {
|
|
27664
|
+
this.enlargeSpaces();
|
|
27665
|
+
}
|
|
27666
|
+
} else {
|
|
27667
|
+
console.log(' ✅ Justify spaces properly expanded');
|
|
27668
|
+
}
|
|
27669
|
+
|
|
27670
|
+
// Ensure height is recalculated - use browser height if available
|
|
27671
|
+
if (this._usingBrowserWrapping && this._actualBrowserHeight) {
|
|
27672
|
+
this.height = this._actualBrowserHeight;
|
|
27673
|
+
console.log(`🔤 JUSTIFY: Preserved browser height: ${this.height}px`);
|
|
27674
|
+
} else {
|
|
27675
|
+
this.height = this.calcTextHeight();
|
|
27676
|
+
console.log(`🔧 JUSTIFY: Used calcTextHeight: ${this.height}px`);
|
|
27677
|
+
}
|
|
27678
|
+
(_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 || _this$canvas6.requestRenderAll();
|
|
27679
|
+
}, 10);
|
|
26115
27680
|
}
|
|
26116
27681
|
}
|
|
26117
27682
|
|