fabric 6.0.2 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/index.js +250 -101
- 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 +250 -101
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +250 -101
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +250 -101
- 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/ClassRegistry.d.ts +1 -0
- package/dist/src/ClassRegistry.d.ts.map +1 -1
- package/dist/src/ClassRegistry.min.mjs +1 -1
- package/dist/src/ClassRegistry.min.mjs.map +1 -1
- package/dist/src/ClassRegistry.mjs +3 -0
- package/dist/src/ClassRegistry.mjs.map +1 -1
- package/dist/src/Collection.d.ts +3 -3
- package/dist/src/Collection.d.ts.map +1 -1
- package/dist/src/EventTypeDefs.d.ts +19 -11
- package/dist/src/EventTypeDefs.d.ts.map +1 -1
- package/dist/src/LayoutManager/LayoutManager.d.ts.map +1 -1
- package/dist/src/LayoutManager/LayoutManager.min.mjs +1 -1
- package/dist/src/LayoutManager/LayoutManager.min.mjs.map +1 -1
- package/dist/src/LayoutManager/LayoutManager.mjs +2 -2
- package/dist/src/LayoutManager/LayoutManager.mjs.map +1 -1
- package/dist/src/Shadow.d.ts.map +1 -1
- package/dist/src/Shadow.min.mjs +1 -1
- package/dist/src/Shadow.min.mjs.map +1 -1
- package/dist/src/Shadow.mjs +1 -5
- package/dist/src/Shadow.mjs.map +1 -1
- package/dist/src/canvas/DOMManagers/util.d.ts.map +1 -1
- package/dist/src/canvas/DOMManagers/util.min.mjs +1 -1
- package/dist/src/canvas/DOMManagers/util.min.mjs.map +1 -1
- package/dist/src/canvas/DOMManagers/util.mjs +9 -15
- package/dist/src/canvas/DOMManagers/util.mjs.map +1 -1
- package/dist/src/canvas/SelectableCanvas.d.ts.map +1 -1
- package/dist/src/canvas/SelectableCanvas.min.mjs +1 -1
- package/dist/src/canvas/SelectableCanvas.min.mjs.map +1 -1
- package/dist/src/canvas/SelectableCanvas.mjs +3 -0
- package/dist/src/canvas/SelectableCanvas.mjs.map +1 -1
- package/dist/src/canvas/StaticCanvas.d.ts +20 -24
- package/dist/src/canvas/StaticCanvas.d.ts.map +1 -1
- package/dist/src/color/util.d.ts.map +1 -1
- package/dist/src/constants.d.ts +1 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.min.mjs +1 -1
- package/dist/src/constants.min.mjs.map +1 -1
- package/dist/src/constants.mjs +2 -1
- package/dist/src/constants.mjs.map +1 -1
- package/dist/src/controls/controlRendering.d.ts +1 -1
- package/dist/src/controls/controlRendering.d.ts.map +1 -1
- package/dist/src/controls/controlRendering.min.mjs.map +1 -1
- package/dist/src/controls/controlRendering.mjs.map +1 -1
- package/dist/src/controls/fireEvent.d.ts +2 -2
- package/dist/src/controls/fireEvent.d.ts.map +1 -1
- package/dist/src/controls/fireEvent.min.mjs.map +1 -1
- package/dist/src/controls/fireEvent.mjs.map +1 -1
- package/dist/src/controls/index.d.ts +1 -0
- package/dist/src/controls/index.d.ts.map +1 -1
- package/dist/src/controls/index.min.mjs +1 -1
- package/dist/src/controls/index.mjs +1 -0
- package/dist/src/controls/index.mjs.map +1 -1
- package/dist/src/controls/pathControl.d.ts +12 -0
- package/dist/src/controls/pathControl.d.ts.map +1 -0
- package/dist/src/controls/pathControl.min.mjs +2 -0
- package/dist/src/controls/pathControl.min.mjs.map +1 -0
- package/dist/src/controls/pathControl.mjs +156 -0
- package/dist/src/controls/pathControl.mjs.map +1 -0
- package/dist/src/controls/polyControl.d.ts.map +1 -1
- package/dist/src/controls/polyControl.min.mjs +1 -1
- package/dist/src/controls/polyControl.min.mjs.map +1 -1
- package/dist/src/controls/polyControl.mjs +1 -9
- package/dist/src/controls/polyControl.mjs.map +1 -1
- package/dist/src/controls/util.d.ts +1 -1
- package/dist/src/controls/wrapWithFireEvent.d.ts +5 -3
- package/dist/src/controls/wrapWithFireEvent.d.ts.map +1 -1
- package/dist/src/controls/wrapWithFireEvent.min.mjs +1 -1
- package/dist/src/controls/wrapWithFireEvent.min.mjs.map +1 -1
- package/dist/src/controls/wrapWithFireEvent.mjs +7 -4
- package/dist/src/controls/wrapWithFireEvent.mjs.map +1 -1
- package/dist/src/controls/wrapWithFixedAnchor.d.ts.map +1 -1
- package/dist/src/env/index.d.ts.map +1 -1
- package/dist/src/env/node.d.ts.map +1 -1
- package/dist/src/filters/BaseFilter.d.ts.map +1 -1
- package/dist/src/filters/BaseFilter.min.mjs.map +1 -1
- package/dist/src/filters/BaseFilter.mjs +0 -1
- package/dist/src/filters/BaseFilter.mjs.map +1 -1
- package/dist/src/filters/ColorMatrix.d.ts.map +1 -1
- package/dist/src/filters/ColorMatrixFilters.d.ts +8 -96
- package/dist/src/filters/ColorMatrixFilters.d.ts.map +1 -1
- package/dist/src/filters/Convolute.d.ts +2 -1
- package/dist/src/filters/Convolute.d.ts.map +1 -1
- package/dist/src/filters/shaders/baseFilter.d.ts +1 -1
- package/dist/src/filters/shaders/baseFilter.d.ts.map +1 -1
- package/dist/src/filters/utils.d.ts.map +1 -1
- package/dist/src/parser/normalizeAttr.d.ts.map +1 -1
- package/dist/src/parser/parseStyleString.d.ts.map +1 -1
- package/dist/src/parser/parseStyleString.min.mjs +1 -1
- package/dist/src/parser/parseStyleString.min.mjs.map +1 -1
- package/dist/src/parser/parseStyleString.mjs +1 -0
- package/dist/src/parser/parseStyleString.mjs.map +1 -1
- package/dist/src/parser/parseUseDirectives.d.ts.map +1 -1
- package/dist/src/parser/parseUseDirectives.min.mjs +1 -1
- package/dist/src/parser/parseUseDirectives.min.mjs.map +1 -1
- package/dist/src/parser/parseUseDirectives.mjs +61 -43
- package/dist/src/parser/parseUseDirectives.mjs.map +1 -1
- package/dist/src/shapes/Group.d.ts +20 -20
- package/dist/src/shapes/Group.d.ts.map +1 -1
- package/dist/src/shapes/IText/DraggableTextDelegate.d.ts.map +1 -1
- package/dist/src/shapes/IText/ITextBehavior.d.ts.map +1 -1
- package/dist/src/shapes/IText/ITextBehavior.min.mjs.map +1 -1
- package/dist/src/shapes/IText/ITextBehavior.mjs +0 -1
- package/dist/src/shapes/IText/ITextBehavior.mjs.map +1 -1
- package/dist/src/shapes/Image.d.ts.map +1 -1
- package/dist/src/shapes/Image.min.mjs.map +1 -1
- package/dist/src/shapes/Image.mjs +0 -2
- package/dist/src/shapes/Image.mjs.map +1 -1
- package/dist/src/shapes/Path.d.ts +9 -9
- package/dist/src/shapes/Path.d.ts.map +1 -1
- package/dist/src/shapes/Polyline.d.ts +1 -1
- package/dist/src/shapes/Rect.d.ts +1 -1
- package/dist/src/shapes/Text/Text.d.ts +1 -1
- package/dist/src/shapes/Text/Text.d.ts.map +1 -1
- package/dist/src/shapes/Text/TextSVGExportMixin.d.ts.map +1 -1
- package/dist/src/shapes/Text/TextSVGExportMixin.min.mjs.map +1 -1
- package/dist/src/shapes/Text/TextSVGExportMixin.mjs +0 -2
- package/dist/src/shapes/Text/TextSVGExportMixin.mjs.map +1 -1
- package/dist/src/shapes/Textbox.d.ts.map +1 -1
- package/dist/src/shapes/Textbox.min.mjs.map +1 -1
- package/dist/src/shapes/Textbox.mjs +0 -2
- package/dist/src/shapes/Textbox.mjs.map +1 -1
- package/dist/src/util/applyMixins.d.ts.map +1 -1
- package/dist/src/util/dom_misc.d.ts.map +1 -1
- package/dist/src/util/dom_misc.min.mjs +1 -1
- package/dist/src/util/dom_misc.min.mjs.map +1 -1
- package/dist/src/util/dom_misc.mjs +7 -9
- package/dist/src/util/dom_misc.mjs.map +1 -1
- package/dist/src/util/internals/cloneDeep.d.ts.map +1 -1
- package/dist/src/util/internals/console.d.ts +1 -1
- package/dist/src/util/internals/findRight.d.ts.map +1 -1
- package/dist/src/util/internals/removeFromArray.d.ts.map +1 -1
- package/dist/src/util/misc/dom.d.ts.map +1 -1
- package/dist/src/util/misc/groupSVGElements.d.ts.map +1 -1
- package/dist/src/util/misc/matrix.d.ts.map +1 -1
- package/dist/src/util/misc/objectEnlive.d.ts +1 -1
- package/dist/src/util/misc/objectEnlive.d.ts.map +1 -1
- package/dist/src/util/misc/objectEnlive.min.mjs +1 -1
- package/dist/src/util/misc/objectEnlive.min.mjs.map +1 -1
- package/dist/src/util/misc/objectEnlive.mjs +7 -11
- package/dist/src/util/misc/objectEnlive.mjs.map +1 -1
- package/dist/src/util/misc/objectTransforms.d.ts.map +1 -1
- package/dist/src/util/misc/pick.d.ts.map +1 -1
- package/dist/src/util/misc/planeChange.d.ts.map +1 -1
- package/dist/src/util/misc/svgParsing.d.ts.map +1 -1
- package/dist/src/util/misc/textStyles.d.ts.map +1 -1
- package/dist/src/util/path/index.d.ts +0 -1
- package/dist/src/util/path/index.d.ts.map +1 -1
- package/dist/src/util/path/index.min.mjs +1 -1
- package/dist/src/util/path/index.min.mjs.map +1 -1
- package/dist/src/util/path/index.mjs +1 -2
- package/dist/src/util/path/index.mjs.map +1 -1
- package/dist/src/util/path/typedefs.d.ts +1 -0
- package/dist/src/util/path/typedefs.d.ts.map +1 -1
- package/dist/src/util/typeAssertions.d.ts +2 -2
- package/dist/src/util/typeAssertions.d.ts.map +1 -1
- package/lib/aligning_guidelines.js +76 -1
- package/lib/centering_guidelines.js +3 -1
- package/package.json +3 -3
- package/src/ClassRegistry.spec.ts +39 -0
- package/src/ClassRegistry.ts +4 -0
- package/src/EventTypeDefs.ts +22 -10
- package/src/LayoutManager/ActiveSelectionLayoutManager.spec.ts +1 -0
- package/src/LayoutManager/LayoutManager.spec.ts +1 -0
- package/src/LayoutManager/LayoutManager.ts +2 -0
- package/src/Shadow.ts +1 -6
- package/src/canvas/DOMManagers/util.ts +11 -15
- package/src/canvas/SelectableCanvas.spec.ts +18 -0
- package/src/canvas/SelectableCanvas.ts +3 -0
- package/src/constants.ts +1 -0
- package/src/controls/controlRendering.ts +4 -2
- package/src/controls/fireEvent.ts +2 -2
- package/src/controls/index.ts +1 -0
- package/src/controls/pathControl.spec.ts +75 -0
- package/src/controls/pathControl.ts +293 -0
- package/src/controls/polyControl.ts +1 -1
- package/src/controls/wrapWithFireEvent.ts +14 -5
- package/src/filters/BaseFilter.ts +1 -2
- package/src/parser/parseStyleString.ts +1 -0
- package/src/parser/parseUseDirectives.test.ts +113 -0
- package/src/parser/parseUseDirectives.ts +64 -58
- package/src/shapes/IText/ITextBehavior.ts +4 -2
- package/src/shapes/Image.ts +0 -2
- package/src/shapes/Text/TextSVGExportMixin.ts +0 -2
- package/src/shapes/Textbox.ts +0 -2
- package/src/util/dom_misc.ts +17 -10
- package/src/util/misc/objectEnlive.spec.ts +68 -0
- package/src/util/misc/objectEnlive.ts +7 -13
- package/src/util/path/__snapshots__/index.spec.ts.snap +327 -0
- package/src/util/path/index.spec.ts +13 -2
- package/src/util/path/index.ts +2 -4
- package/src/util/path/typedefs.ts +2 -0
- package/.gitmodules +0 -3
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { Point } from '../Point';
|
|
2
|
+
import { Control } from './Control';
|
|
3
|
+
import type { TMat2D } from '../typedefs';
|
|
4
|
+
import type { Path } from '../shapes/Path';
|
|
5
|
+
import { multiplyTransformMatrices } from '../util/misc/matrix';
|
|
6
|
+
import type {
|
|
7
|
+
TModificationEvents,
|
|
8
|
+
TPointerEvent,
|
|
9
|
+
Transform,
|
|
10
|
+
} from '../EventTypeDefs';
|
|
11
|
+
import { sendPointToPlane } from '../util/misc/planeChange';
|
|
12
|
+
import type { TSimpleParseCommandType } from '../util/path/typedefs';
|
|
13
|
+
import type { ControlRenderingStyleOverride } from './controlRendering';
|
|
14
|
+
import { fireEvent } from './fireEvent';
|
|
15
|
+
import { commonEventInfo } from './util';
|
|
16
|
+
|
|
17
|
+
const ACTION_NAME: TModificationEvents = 'modifyPath' as const;
|
|
18
|
+
|
|
19
|
+
type TTransformAnchor = Transform;
|
|
20
|
+
|
|
21
|
+
export type PathPointControlStyle = {
|
|
22
|
+
controlFill?: string;
|
|
23
|
+
controlStroke?: string;
|
|
24
|
+
connectionDashArray?: number[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const calcPathPointPosition = (
|
|
28
|
+
pathObject: Path,
|
|
29
|
+
commandIndex: number,
|
|
30
|
+
pointIndex: number
|
|
31
|
+
) => {
|
|
32
|
+
const { path, pathOffset } = pathObject;
|
|
33
|
+
const command = path[commandIndex];
|
|
34
|
+
return new Point(
|
|
35
|
+
(command[pointIndex] as number) - pathOffset.x,
|
|
36
|
+
(command[pointIndex + 1] as number) - pathOffset.y
|
|
37
|
+
).transform(
|
|
38
|
+
multiplyTransformMatrices(
|
|
39
|
+
pathObject.getViewportTransform(),
|
|
40
|
+
pathObject.calcTransformMatrix()
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const movePathPoint = (
|
|
46
|
+
pathObject: Path,
|
|
47
|
+
x: number,
|
|
48
|
+
y: number,
|
|
49
|
+
commandIndex: number,
|
|
50
|
+
pointIndex: number
|
|
51
|
+
) => {
|
|
52
|
+
const { path, pathOffset } = pathObject;
|
|
53
|
+
|
|
54
|
+
const anchorCommand =
|
|
55
|
+
path[(commandIndex > 0 ? commandIndex : path.length) - 1];
|
|
56
|
+
const anchorPoint = new Point(
|
|
57
|
+
anchorCommand[pointIndex] as number,
|
|
58
|
+
anchorCommand[pointIndex + 1] as number
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const anchorPointInParentPlane = anchorPoint
|
|
62
|
+
.subtract(pathOffset)
|
|
63
|
+
.transform(pathObject.calcOwnMatrix());
|
|
64
|
+
|
|
65
|
+
const mouseLocalPosition = sendPointToPlane(
|
|
66
|
+
new Point(x, y),
|
|
67
|
+
undefined,
|
|
68
|
+
pathObject.calcOwnMatrix()
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
path[commandIndex][pointIndex] = mouseLocalPosition.x + pathOffset.x;
|
|
72
|
+
path[commandIndex][pointIndex + 1] = mouseLocalPosition.y + pathOffset.y;
|
|
73
|
+
pathObject.setDimensions();
|
|
74
|
+
|
|
75
|
+
const newAnchorPointInParentPlane = anchorPoint
|
|
76
|
+
.subtract(pathObject.pathOffset)
|
|
77
|
+
.transform(pathObject.calcOwnMatrix());
|
|
78
|
+
|
|
79
|
+
const diff = newAnchorPointInParentPlane.subtract(anchorPointInParentPlane);
|
|
80
|
+
pathObject.left -= diff.x;
|
|
81
|
+
pathObject.top -= diff.y;
|
|
82
|
+
pathObject.set('dirty', true);
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* This function locates the controls.
|
|
88
|
+
* It'll be used both for drawing and for interaction.
|
|
89
|
+
*/
|
|
90
|
+
function pathPositionHandler(
|
|
91
|
+
this: PathPointControl,
|
|
92
|
+
dim: Point,
|
|
93
|
+
finalMatrix: TMat2D,
|
|
94
|
+
pathObject: Path
|
|
95
|
+
) {
|
|
96
|
+
const { commandIndex, pointIndex } = this;
|
|
97
|
+
return calcPathPointPosition(pathObject, commandIndex, pointIndex);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* This function defines what the control does.
|
|
102
|
+
* It'll be called on every mouse move after a control has been clicked and is being dragged.
|
|
103
|
+
* The function receives as argument the mouse event, the current transform object
|
|
104
|
+
* and the current position in canvas coordinate `transform.target` is a reference to the
|
|
105
|
+
* current object being transformed.
|
|
106
|
+
*/
|
|
107
|
+
function pathActionHandler(
|
|
108
|
+
this: PathPointControl,
|
|
109
|
+
eventData: TPointerEvent,
|
|
110
|
+
transform: TTransformAnchor,
|
|
111
|
+
x: number,
|
|
112
|
+
y: number
|
|
113
|
+
) {
|
|
114
|
+
const { target } = transform;
|
|
115
|
+
const { commandIndex, pointIndex } = this;
|
|
116
|
+
const actionPerformed = movePathPoint(
|
|
117
|
+
target as Path,
|
|
118
|
+
x,
|
|
119
|
+
y,
|
|
120
|
+
commandIndex,
|
|
121
|
+
pointIndex
|
|
122
|
+
);
|
|
123
|
+
if (actionPerformed) {
|
|
124
|
+
fireEvent(this.actionName as TModificationEvents, {
|
|
125
|
+
...commonEventInfo(eventData, transform, x, y),
|
|
126
|
+
commandIndex,
|
|
127
|
+
pointIndex,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return actionPerformed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const indexFromPrevCommand = (previousCommandType: TSimpleParseCommandType) =>
|
|
134
|
+
previousCommandType === 'C' ? 5 : previousCommandType === 'Q' ? 3 : 1;
|
|
135
|
+
|
|
136
|
+
class PathPointControl extends Control {
|
|
137
|
+
declare commandIndex: number;
|
|
138
|
+
declare pointIndex: number;
|
|
139
|
+
declare controlFill: string;
|
|
140
|
+
declare controlStroke: string;
|
|
141
|
+
constructor(options?: Partial<PathPointControl>) {
|
|
142
|
+
super(options);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
render(
|
|
146
|
+
ctx: CanvasRenderingContext2D,
|
|
147
|
+
left: number,
|
|
148
|
+
top: number,
|
|
149
|
+
styleOverride: ControlRenderingStyleOverride | undefined,
|
|
150
|
+
fabricObject: Path
|
|
151
|
+
) {
|
|
152
|
+
const overrides: ControlRenderingStyleOverride = {
|
|
153
|
+
...styleOverride,
|
|
154
|
+
cornerColor: this.controlFill,
|
|
155
|
+
cornerStrokeColor: this.controlStroke,
|
|
156
|
+
transparentCorners: !this.controlFill,
|
|
157
|
+
};
|
|
158
|
+
super.render(ctx, left, top, overrides, fabricObject);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
class PathControlPointControl extends PathPointControl {
|
|
163
|
+
declare connectionDashArray?: number[];
|
|
164
|
+
declare connectToCommandIndex: number;
|
|
165
|
+
declare connectToPointIndex: number;
|
|
166
|
+
constructor(options?: Partial<PathControlPointControl>) {
|
|
167
|
+
super(options);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
render(
|
|
171
|
+
this: PathControlPointControl,
|
|
172
|
+
ctx: CanvasRenderingContext2D,
|
|
173
|
+
left: number,
|
|
174
|
+
top: number,
|
|
175
|
+
styleOverride: ControlRenderingStyleOverride | undefined,
|
|
176
|
+
fabricObject: Path
|
|
177
|
+
) {
|
|
178
|
+
const { path } = fabricObject;
|
|
179
|
+
const {
|
|
180
|
+
commandIndex,
|
|
181
|
+
pointIndex,
|
|
182
|
+
connectToCommandIndex,
|
|
183
|
+
connectToPointIndex,
|
|
184
|
+
} = this;
|
|
185
|
+
ctx.save();
|
|
186
|
+
ctx.strokeStyle = this.controlStroke;
|
|
187
|
+
if (this.connectionDashArray) {
|
|
188
|
+
ctx.setLineDash(this.connectionDashArray);
|
|
189
|
+
}
|
|
190
|
+
const [commandType] = path[commandIndex];
|
|
191
|
+
const point = calcPathPointPosition(
|
|
192
|
+
fabricObject,
|
|
193
|
+
connectToCommandIndex,
|
|
194
|
+
connectToPointIndex
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (commandType === 'Q') {
|
|
198
|
+
// one control point connects to 2 points
|
|
199
|
+
const point2 = calcPathPointPosition(
|
|
200
|
+
fabricObject,
|
|
201
|
+
commandIndex,
|
|
202
|
+
pointIndex + 2
|
|
203
|
+
);
|
|
204
|
+
ctx.moveTo(point2.x, point2.y);
|
|
205
|
+
ctx.lineTo(left, top);
|
|
206
|
+
} else {
|
|
207
|
+
ctx.moveTo(left, top);
|
|
208
|
+
}
|
|
209
|
+
ctx.lineTo(point.x, point.y);
|
|
210
|
+
ctx.stroke();
|
|
211
|
+
ctx.restore();
|
|
212
|
+
|
|
213
|
+
super.render(ctx, left, top, styleOverride, fabricObject);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const createControl = (
|
|
218
|
+
commandIndexPos: number,
|
|
219
|
+
pointIndexPos: number,
|
|
220
|
+
isControlPoint: boolean,
|
|
221
|
+
options: Partial<Control> & {
|
|
222
|
+
controlPointStyle?: PathPointControlStyle;
|
|
223
|
+
pointStyle?: PathPointControlStyle;
|
|
224
|
+
},
|
|
225
|
+
connectToCommandIndex?: number,
|
|
226
|
+
connectToPointIndex?: number
|
|
227
|
+
) =>
|
|
228
|
+
new (isControlPoint ? PathControlPointControl : PathPointControl)({
|
|
229
|
+
commandIndex: commandIndexPos,
|
|
230
|
+
pointIndex: pointIndexPos,
|
|
231
|
+
actionName: ACTION_NAME,
|
|
232
|
+
positionHandler: pathPositionHandler,
|
|
233
|
+
actionHandler: pathActionHandler,
|
|
234
|
+
connectToCommandIndex,
|
|
235
|
+
connectToPointIndex,
|
|
236
|
+
...options,
|
|
237
|
+
...(isControlPoint ? options.controlPointStyle : options.pointStyle),
|
|
238
|
+
} as Partial<PathControlPointControl>);
|
|
239
|
+
|
|
240
|
+
export function createPathControls(
|
|
241
|
+
path: Path,
|
|
242
|
+
options: Partial<Control> & {
|
|
243
|
+
controlPointStyle?: PathPointControlStyle;
|
|
244
|
+
pointStyle?: PathPointControlStyle;
|
|
245
|
+
} = {}
|
|
246
|
+
): Record<string, Control> {
|
|
247
|
+
const controls = {} as Record<string, Control>;
|
|
248
|
+
let previousCommandType: TSimpleParseCommandType = 'M';
|
|
249
|
+
path.path.forEach((command, commandIndex) => {
|
|
250
|
+
const commandType = command[0];
|
|
251
|
+
|
|
252
|
+
if (commandType !== 'Z') {
|
|
253
|
+
controls[`c_${commandIndex}_${commandType}`] = createControl(
|
|
254
|
+
commandIndex,
|
|
255
|
+
command.length - 2,
|
|
256
|
+
false,
|
|
257
|
+
options
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
switch (commandType) {
|
|
261
|
+
case 'C':
|
|
262
|
+
controls[`c_${commandIndex}_C_CP_1`] = createControl(
|
|
263
|
+
commandIndex,
|
|
264
|
+
1,
|
|
265
|
+
true,
|
|
266
|
+
options,
|
|
267
|
+
commandIndex - 1,
|
|
268
|
+
indexFromPrevCommand(previousCommandType)
|
|
269
|
+
);
|
|
270
|
+
controls[`c_${commandIndex}_C_CP_2`] = createControl(
|
|
271
|
+
commandIndex,
|
|
272
|
+
3,
|
|
273
|
+
true,
|
|
274
|
+
options,
|
|
275
|
+
commandIndex,
|
|
276
|
+
5
|
|
277
|
+
);
|
|
278
|
+
break;
|
|
279
|
+
case 'Q':
|
|
280
|
+
controls[`c_${commandIndex}_Q_CP_1`] = createControl(
|
|
281
|
+
commandIndex,
|
|
282
|
+
1,
|
|
283
|
+
true,
|
|
284
|
+
options,
|
|
285
|
+
commandIndex,
|
|
286
|
+
3
|
|
287
|
+
);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
previousCommandType = commandType;
|
|
291
|
+
});
|
|
292
|
+
return controls;
|
|
293
|
+
}
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
TransformActionHandler,
|
|
11
11
|
} from '../EventTypeDefs';
|
|
12
12
|
import { wrapWithFireEvent } from './wrapWithFireEvent';
|
|
13
|
-
import { sendPointToPlane } from '../util';
|
|
13
|
+
import { sendPointToPlane } from '../util/misc/planeChange';
|
|
14
14
|
import { MODIFY_POLY } from '../constants';
|
|
15
15
|
|
|
16
16
|
const ACTION_NAME: TModificationEvents = MODIFY_POLY;
|
|
@@ -8,17 +8,26 @@ import { commonEventInfo } from './util';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Wrap an action handler with firing an event if the action is performed
|
|
11
|
-
* @param {
|
|
12
|
-
* @
|
|
11
|
+
* @param {TModificationEvents} eventName the event we want to fire
|
|
12
|
+
* @param {TransformActionHandler<T>} actionHandler the function to wrap
|
|
13
|
+
* @param {object} extraEventInfo extra information to pas to the event handler
|
|
14
|
+
* @return {TransformActionHandler<T>} a function with an action handler signature
|
|
13
15
|
*/
|
|
14
|
-
export const wrapWithFireEvent = <
|
|
16
|
+
export const wrapWithFireEvent = <
|
|
17
|
+
T extends Transform,
|
|
18
|
+
P extends object = Record<string, never>
|
|
19
|
+
>(
|
|
15
20
|
eventName: TModificationEvents,
|
|
16
|
-
actionHandler: TransformActionHandler<T
|
|
21
|
+
actionHandler: TransformActionHandler<T>,
|
|
22
|
+
extraEventInfo?: P
|
|
17
23
|
) => {
|
|
18
24
|
return ((eventData, transform, x, y) => {
|
|
19
25
|
const actionPerformed = actionHandler(eventData, transform, x, y);
|
|
20
26
|
if (actionPerformed) {
|
|
21
|
-
fireEvent(eventName,
|
|
27
|
+
fireEvent(eventName, {
|
|
28
|
+
...commonEventInfo(eventData, transform, x, y),
|
|
29
|
+
...extraEventInfo,
|
|
30
|
+
});
|
|
22
31
|
}
|
|
23
32
|
return actionPerformed;
|
|
24
33
|
}) as TransformActionHandler<T>;
|
|
@@ -387,8 +387,7 @@ export class BaseFilter<
|
|
|
387
387
|
return {
|
|
388
388
|
type: this.type,
|
|
389
389
|
...defaultKeys.reduce<OwnProps>((acc, key) => {
|
|
390
|
-
|
|
391
|
-
acc[key] = this[key as keyof this];
|
|
390
|
+
acc[key] = this[key as keyof this] as unknown as typeof acc[typeof key];
|
|
392
391
|
return acc;
|
|
393
392
|
}, {} as OwnProps),
|
|
394
393
|
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getFabricWindow } from '../env';
|
|
2
|
+
import { parseUseDirectives } from './parseUseDirectives';
|
|
3
|
+
|
|
4
|
+
describe('parseUseDirectives', () => {
|
|
5
|
+
it('returns successful parse where use tag uses fill style prioritizing path tag when both tags have a style', async () => {
|
|
6
|
+
const str = `<svg id="svg" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
7
|
+
<path id="heart" d="M10,30 A20,20,0,0,1,50,30 A20,20,0,0,1,90,30 Q90,60,50,90 Q10,60,10,30 Z"
|
|
8
|
+
style="stroke:#000000;fill:#ff0000" />
|
|
9
|
+
<use x="100" y="0" xlink:href="#heart" style="stroke-width:5.0;fill:#0000ff" />
|
|
10
|
+
</svg>`;
|
|
11
|
+
|
|
12
|
+
const parser = new (getFabricWindow().DOMParser)();
|
|
13
|
+
const doc = parser.parseFromString(str.trim(), 'text/xml');
|
|
14
|
+
parseUseDirectives(doc);
|
|
15
|
+
|
|
16
|
+
const elements = Array.from(doc.documentElement.getElementsByTagName('*'));
|
|
17
|
+
expect(elements[0]).not.toBeNull();
|
|
18
|
+
if (elements[0] !== null) {
|
|
19
|
+
const style0 = elements[0].getAttribute('style');
|
|
20
|
+
expect(style0).toContain('fill:#ff0000');
|
|
21
|
+
}
|
|
22
|
+
expect(elements[1]).not.toBeNull();
|
|
23
|
+
if (elements[1] !== null) {
|
|
24
|
+
const style1 = elements[1].getAttribute('style');
|
|
25
|
+
expect(style1).toContain('fill:#ff0000');
|
|
26
|
+
// also contains extra style that path tag does not have
|
|
27
|
+
expect(style1).toContain('stroke-width:5.0');
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
it('returns successful parse where use tag uses fill style from itself when path tag empty', async () => {
|
|
31
|
+
const str = `<svg id="svg" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
32
|
+
<path id="heart" d="M10,30 A20,20,0,0,1,50,30 A20,20,0,0,1,90,30 Q90,60,50,90 Q10,60,10,30 Z" />
|
|
33
|
+
<use x="100" y="0" xlink:href="#heart" style="stroke-width:5.0;fill:#0000ff" />
|
|
34
|
+
</svg>`;
|
|
35
|
+
|
|
36
|
+
const parser = new (getFabricWindow().DOMParser)();
|
|
37
|
+
const doc = parser.parseFromString(str.trim(), 'text/xml');
|
|
38
|
+
parseUseDirectives(doc);
|
|
39
|
+
|
|
40
|
+
const elements = Array.from(doc.documentElement.getElementsByTagName('*'));
|
|
41
|
+
expect(elements[0]).not.toBeNull();
|
|
42
|
+
if (elements[0] !== null) {
|
|
43
|
+
const style0 = elements[0].getAttribute('style');
|
|
44
|
+
expect(style0).toBeNull();
|
|
45
|
+
}
|
|
46
|
+
expect(elements[1]).not.toBeNull();
|
|
47
|
+
if (elements[1] !== null) {
|
|
48
|
+
const style1 = elements[1].getAttribute('style');
|
|
49
|
+
expect(style1).toContain('fill:#0000ff');
|
|
50
|
+
// also contains extra style that path tag does not have
|
|
51
|
+
expect(style1).toContain('stroke-width:5.0');
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
it('returns successful parse where use tag uses fill style from path when its style tag is empty', async () => {
|
|
55
|
+
const str = `<svg id="svg" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
56
|
+
<path id="heart" d="M10,30 A20,20,0,0,1,50,30 A20,20,0,0,1,90,30 Q90,60,50,90 Q10,60,10,30 Z"
|
|
57
|
+
style="stroke:#000000;fill:#ff0000" />
|
|
58
|
+
<use x="100" y="0" xlink:href="#heart" />
|
|
59
|
+
</svg>`;
|
|
60
|
+
|
|
61
|
+
const parser = new (getFabricWindow().DOMParser)();
|
|
62
|
+
const doc = parser.parseFromString(str.trim(), 'text/xml');
|
|
63
|
+
parseUseDirectives(doc);
|
|
64
|
+
|
|
65
|
+
const elements = Array.from(doc.documentElement.getElementsByTagName('*'));
|
|
66
|
+
expect(elements[0]).not.toBeNull();
|
|
67
|
+
if (elements[0] !== null) {
|
|
68
|
+
const style0 = elements[0].getAttribute('style');
|
|
69
|
+
expect(style0).toContain('fill:#ff0000');
|
|
70
|
+
}
|
|
71
|
+
expect(elements[1]).not.toBeNull();
|
|
72
|
+
if (elements[1] !== null) {
|
|
73
|
+
const style1 = elements[1].getAttribute('style');
|
|
74
|
+
expect(style1).toContain('fill:#ff0000');
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
it('correctly merge styles tags considering attributes', async () => {
|
|
78
|
+
const str = `<svg id="svg" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
79
|
+
<path fill="red" id="heart" d="M10,30 A20,20,0,0,1,50,30 A20,20,0,0,1,90,30 Q90,60,50,90 Q10,60,10,30 Z" />
|
|
80
|
+
<use x="100" y="0" xlink:href="#heart" style="stroke:#000000;fill:green" />
|
|
81
|
+
</svg>`;
|
|
82
|
+
|
|
83
|
+
const parser = new (getFabricWindow().DOMParser)();
|
|
84
|
+
const doc = parser.parseFromString(str.trim(), 'text/xml');
|
|
85
|
+
parseUseDirectives(doc);
|
|
86
|
+
|
|
87
|
+
const elements = Array.from(doc.documentElement.getElementsByTagName('*'));
|
|
88
|
+
expect(elements[0]).not.toBeNull();
|
|
89
|
+
expect(elements[1]).not.toBeNull();
|
|
90
|
+
if (elements[1] !== null) {
|
|
91
|
+
const style1 = elements[1].getAttribute('style');
|
|
92
|
+
expect(style1).toContain('fill:red');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
it('Will not override existing attributes', async () => {
|
|
96
|
+
const str = `<svg id="svg" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
97
|
+
<path fill="yellow" id="heart" d="M10,30 A20,20,0,0,1,50,30 A20,20,0,0,1,90,30 Q90,60,50,90 Q10,60,10,30 Z" />
|
|
98
|
+
<use x="100" y="0" xlink:href="#heart" fill="blue" />
|
|
99
|
+
</svg>`;
|
|
100
|
+
|
|
101
|
+
const parser = new (getFabricWindow().DOMParser)();
|
|
102
|
+
const doc = parser.parseFromString(str.trim(), 'text/xml');
|
|
103
|
+
parseUseDirectives(doc);
|
|
104
|
+
|
|
105
|
+
const elements = Array.from(doc.documentElement.getElementsByTagName('*'));
|
|
106
|
+
expect(elements[0]).not.toBeNull();
|
|
107
|
+
expect(elements[1]).not.toBeNull();
|
|
108
|
+
if (elements[1] !== null) {
|
|
109
|
+
const style1 = elements[1].getAttribute('fill');
|
|
110
|
+
expect(style1).toBe('yellow');
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -1,85 +1,91 @@
|
|
|
1
1
|
import { svgNS } from './constants';
|
|
2
2
|
import { getMultipleNodes } from './getMultipleNodes';
|
|
3
3
|
import { applyViewboxTransform } from './applyViewboxTransform';
|
|
4
|
+
import { parseStyleString } from './parseStyleString';
|
|
4
5
|
|
|
5
6
|
export function parseUseDirectives(doc: Document) {
|
|
6
7
|
const nodelist = getMultipleNodes(doc, ['use', 'svg:use']);
|
|
7
|
-
|
|
8
|
-
while (nodelist.length && i < nodelist.length) {
|
|
9
|
-
const el = nodelist[i],
|
|
10
|
-
xlinkAttribute = el.getAttribute('xlink:href') || el.getAttribute('href');
|
|
8
|
+
const skipAttributes = ['x', 'y', 'xlink:href', 'href', 'transform'];
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
for (const useElement of nodelist) {
|
|
11
|
+
const useAttributes: NamedNodeMap = useElement.attributes;
|
|
12
|
+
|
|
13
|
+
const useAttrMap: Record<string, string> = {};
|
|
14
|
+
for (const attr of useAttributes) {
|
|
15
|
+
attr.value && (useAttrMap[attr.name] = attr.value);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
const xlink =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const xlink = (useAttrMap['xlink:href'] || useAttrMap.href || '').slice(1);
|
|
19
|
+
|
|
20
|
+
if (xlink === '') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const referencedElement = doc.getElementById(xlink);
|
|
24
|
+
if (referencedElement === null) {
|
|
21
25
|
// if we can't find the target of the xlink, consider this use tag bad, similar to no xlink
|
|
22
26
|
return;
|
|
23
27
|
}
|
|
24
|
-
let
|
|
25
|
-
let currentTrans =
|
|
26
|
-
(el2.getAttribute('transform') || '') +
|
|
27
|
-
' translate(' +
|
|
28
|
-
x +
|
|
29
|
-
', ' +
|
|
30
|
-
y +
|
|
31
|
-
')';
|
|
32
|
-
const oldLength = nodelist.length;
|
|
33
|
-
const namespace = svgNS;
|
|
28
|
+
let clonedOriginal = referencedElement.cloneNode(true) as Element;
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
const originalAttributes: NamedNodeMap = clonedOriginal.attributes;
|
|
31
|
+
|
|
32
|
+
const originalAttrMap: Record<string, string> = {};
|
|
33
|
+
for (const attr of originalAttributes) {
|
|
34
|
+
attr.value && (originalAttrMap[attr.name] = attr.value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Transform attribute needs to be merged in a particular way
|
|
38
|
+
const { x = 0, y = 0, transform = '' } = useAttrMap;
|
|
39
|
+
const currentTrans = `${transform} ${
|
|
40
|
+
originalAttrMap.transform || ''
|
|
41
|
+
} translate(${x}, ${y})`;
|
|
42
|
+
|
|
43
|
+
applyViewboxTransform(clonedOriginal);
|
|
44
|
+
|
|
45
|
+
if (/^svg$/i.test(clonedOriginal.nodeName)) {
|
|
46
|
+
// if is an SVG, create a group and apply all the attributes on top of it
|
|
47
|
+
const el3 = clonedOriginal.ownerDocument.createElementNS(svgNS, 'g');
|
|
48
|
+
Object.entries(originalAttrMap).forEach(([name, value]) =>
|
|
49
|
+
el3.setAttributeNS(svgNS, name, value)
|
|
50
|
+
);
|
|
51
|
+
el3.append(...clonedOriginal.childNodes);
|
|
52
|
+
clonedOriginal = el3;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
for (
|
|
54
|
-
const attr = attrs.item(j);
|
|
55
|
+
for (const attr of useAttributes) {
|
|
55
56
|
if (!attr) {
|
|
56
57
|
continue;
|
|
57
58
|
}
|
|
58
|
-
const {
|
|
59
|
-
if (
|
|
60
|
-
nodeName === 'x' ||
|
|
61
|
-
nodeName === 'y' ||
|
|
62
|
-
nodeName === 'xlink:href' ||
|
|
63
|
-
nodeName === 'href'
|
|
64
|
-
) {
|
|
59
|
+
const { name, value } = attr;
|
|
60
|
+
if (skipAttributes.includes(name)) {
|
|
65
61
|
continue;
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
if (
|
|
69
|
-
|
|
64
|
+
if (name === 'style') {
|
|
65
|
+
// when use has a style, merge the two styles, with the ref being priority (not use)
|
|
66
|
+
// priority is by feature. an attribute for fill on the original element
|
|
67
|
+
// will overwrite the fill in style or attribute for tha use
|
|
68
|
+
const styleRecord: Record<string, any> = {};
|
|
69
|
+
parseStyleString(value!, styleRecord);
|
|
70
|
+
// cleanup styleRecord from attributes of original
|
|
71
|
+
Object.entries(originalAttrMap).forEach(([name, value]) => {
|
|
72
|
+
styleRecord[name] = value;
|
|
73
|
+
});
|
|
74
|
+
// now we can put in the style of the original that will overwrite the original attributes
|
|
75
|
+
parseStyleString(originalAttrMap.style || '', styleRecord);
|
|
76
|
+
const mergedStyles = Object.entries(styleRecord)
|
|
77
|
+
.map((entry) => entry.join(':'))
|
|
78
|
+
.join(';');
|
|
79
|
+
clonedOriginal.setAttribute(name, mergedStyles);
|
|
70
80
|
} else {
|
|
71
|
-
|
|
81
|
+
// set the attribute from use element only if the original does not have it already
|
|
82
|
+
!originalAttrMap[name] && clonedOriginal.setAttribute(name, value!);
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
parentNode!.replaceChild(el2, el);
|
|
80
|
-
// some browsers do not shorten nodelist after replaceChild (IE8)
|
|
81
|
-
if (nodelist.length === oldLength) {
|
|
82
|
-
i++;
|
|
83
|
-
}
|
|
86
|
+
clonedOriginal.setAttribute('transform', currentTrans);
|
|
87
|
+
clonedOriginal.setAttribute('instantiated_by_use', '1');
|
|
88
|
+
clonedOriginal.removeAttribute('id');
|
|
89
|
+
useElement.parentNode!.replaceChild(clonedOriginal, useElement);
|
|
84
90
|
}
|
|
85
91
|
}
|
|
@@ -406,8 +406,10 @@ export abstract class ITextBehavior<
|
|
|
406
406
|
this.fire('editing:entered', e ? { e } : undefined);
|
|
407
407
|
this._fireSelectionChanged();
|
|
408
408
|
if (this.canvas) {
|
|
409
|
-
|
|
410
|
-
|
|
409
|
+
this.canvas.fire('text:editing:entered', {
|
|
410
|
+
target: this as unknown as IText,
|
|
411
|
+
e,
|
|
412
|
+
});
|
|
411
413
|
this.canvas.requestRenderAll();
|
|
412
414
|
}
|
|
413
415
|
}
|
package/src/shapes/Image.ts
CHANGED
|
@@ -607,8 +607,6 @@ export class FabricImage<
|
|
|
607
607
|
ctx: CanvasRenderingContext2D
|
|
608
608
|
) {
|
|
609
609
|
ctx.imageSmoothingEnabled = this.imageSmoothing;
|
|
610
|
-
// cant use ts-expect-error because of ts 5.3 cross check
|
|
611
|
-
// @ts-ignore TS doesn't respect this type casting
|
|
612
610
|
super.drawCacheOnCanvas(ctx);
|
|
613
611
|
}
|
|
614
612
|
|
|
@@ -297,8 +297,6 @@ export class TextSVGExportMixin extends FabricObjectSVGExportMixin {
|
|
|
297
297
|
* @return {String}
|
|
298
298
|
*/
|
|
299
299
|
getSvgStyles(this: TextSVGExportMixin & FabricText, skipShadow?: boolean) {
|
|
300
|
-
// cant use ts-expect-error because of ts 5.3 cross check
|
|
301
|
-
// @ts-ignore TS doesn't respect this type casting
|
|
302
300
|
return `${super.getSvgStyles(skipShadow)} white-space: pre;`;
|
|
303
301
|
}
|
|
304
302
|
|
package/src/shapes/Textbox.ts
CHANGED
|
@@ -568,8 +568,6 @@ export class Textbox<
|
|
|
568
568
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
569
569
|
* @return {Object} object representation of an instance
|
|
570
570
|
*/
|
|
571
|
-
// cant use ts-expect-error because of ts 5.3 cross check
|
|
572
|
-
// @ts-ignore TS this typing limitations
|
|
573
571
|
toObject<
|
|
574
572
|
T extends Omit<Props & TClassProperties<this>, keyof SProps>,
|
|
575
573
|
K extends keyof T = never
|