@pooder/kit 6.1.0 → 6.1.2
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 +14 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +285 -2
- package/dist/index.mjs +286 -2
- package/package.json +2 -2
- package/src/extensions/image/ImageTool.ts +377 -3
package/CHANGELOG.md
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -92,6 +92,11 @@ declare class ImageTool implements Extension {
|
|
|
92
92
|
private cropShapeHatchPatternKey?;
|
|
93
93
|
private imageSpecs;
|
|
94
94
|
private overlaySpecs;
|
|
95
|
+
private activeSnapX;
|
|
96
|
+
private activeSnapY;
|
|
97
|
+
private snapGuideXObject?;
|
|
98
|
+
private snapGuideYObject?;
|
|
99
|
+
private canvasObjectMovingHandler?;
|
|
95
100
|
private renderProducerDisposable?;
|
|
96
101
|
private readonly subscriptions;
|
|
97
102
|
private imageControlsByCapabilityKey;
|
|
@@ -102,6 +107,21 @@ declare class ImageTool implements Extension {
|
|
|
102
107
|
private onSelectionCleared;
|
|
103
108
|
private onSceneLayoutChanged;
|
|
104
109
|
private onSceneGeometryChanged;
|
|
110
|
+
private bindCanvasInteractionHandlers;
|
|
111
|
+
private unbindCanvasInteractionHandlers;
|
|
112
|
+
private getActiveImageTarget;
|
|
113
|
+
private getTargetBoundsScene;
|
|
114
|
+
private getSnapThresholdScene;
|
|
115
|
+
private pickSnapMatch;
|
|
116
|
+
private computeMoveSnapMatches;
|
|
117
|
+
private areSnapMatchesEqual;
|
|
118
|
+
private updateSnapMatchState;
|
|
119
|
+
private clearSnapGuides;
|
|
120
|
+
private removeSnapGuideObject;
|
|
121
|
+
private createOrUpdateSnapGuideObject;
|
|
122
|
+
private updateSnapGuideVisuals;
|
|
123
|
+
private handleCanvasObjectMoving;
|
|
124
|
+
private applySnapMatchesToTarget;
|
|
105
125
|
private syncToolActiveFromWorkbench;
|
|
106
126
|
private isImageEditingVisible;
|
|
107
127
|
private getEnabledImageControlCapabilities;
|
package/dist/index.d.ts
CHANGED
|
@@ -92,6 +92,11 @@ declare class ImageTool implements Extension {
|
|
|
92
92
|
private cropShapeHatchPatternKey?;
|
|
93
93
|
private imageSpecs;
|
|
94
94
|
private overlaySpecs;
|
|
95
|
+
private activeSnapX;
|
|
96
|
+
private activeSnapY;
|
|
97
|
+
private snapGuideXObject?;
|
|
98
|
+
private snapGuideYObject?;
|
|
99
|
+
private canvasObjectMovingHandler?;
|
|
95
100
|
private renderProducerDisposable?;
|
|
96
101
|
private readonly subscriptions;
|
|
97
102
|
private imageControlsByCapabilityKey;
|
|
@@ -102,6 +107,21 @@ declare class ImageTool implements Extension {
|
|
|
102
107
|
private onSelectionCleared;
|
|
103
108
|
private onSceneLayoutChanged;
|
|
104
109
|
private onSceneGeometryChanged;
|
|
110
|
+
private bindCanvasInteractionHandlers;
|
|
111
|
+
private unbindCanvasInteractionHandlers;
|
|
112
|
+
private getActiveImageTarget;
|
|
113
|
+
private getTargetBoundsScene;
|
|
114
|
+
private getSnapThresholdScene;
|
|
115
|
+
private pickSnapMatch;
|
|
116
|
+
private computeMoveSnapMatches;
|
|
117
|
+
private areSnapMatchesEqual;
|
|
118
|
+
private updateSnapMatchState;
|
|
119
|
+
private clearSnapGuides;
|
|
120
|
+
private removeSnapGuideObject;
|
|
121
|
+
private createOrUpdateSnapGuideObject;
|
|
122
|
+
private updateSnapGuideVisuals;
|
|
123
|
+
private handleCanvasObjectMoving;
|
|
124
|
+
private applySnapMatchesToTarget;
|
|
105
125
|
private syncToolActiveFromWorkbench;
|
|
106
126
|
private isImageEditingVisible;
|
|
107
127
|
private getEnabledImageControlCapabilities;
|
package/dist/index.js
CHANGED
|
@@ -3074,6 +3074,9 @@ var IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
|
|
|
3074
3074
|
"rotate",
|
|
3075
3075
|
"scale"
|
|
3076
3076
|
];
|
|
3077
|
+
var IMAGE_MOVE_SNAP_THRESHOLD_PX = 6;
|
|
3078
|
+
var IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX = 10;
|
|
3079
|
+
var IMAGE_SNAP_GUIDE_LAYER_ID = "image.snapGuide";
|
|
3077
3080
|
var IMAGE_CONTROL_DESCRIPTORS = [
|
|
3078
3081
|
{
|
|
3079
3082
|
key: "tl",
|
|
@@ -3118,12 +3121,15 @@ var ImageTool = class {
|
|
|
3118
3121
|
this.renderSeq = 0;
|
|
3119
3122
|
this.imageSpecs = [];
|
|
3120
3123
|
this.overlaySpecs = [];
|
|
3124
|
+
this.activeSnapX = null;
|
|
3125
|
+
this.activeSnapY = null;
|
|
3121
3126
|
this.subscriptions = new SubscriptionBag();
|
|
3122
3127
|
this.imageControlsByCapabilityKey = /* @__PURE__ */ new Map();
|
|
3123
3128
|
this.onToolActivated = (event) => {
|
|
3124
3129
|
const before = this.isToolActive;
|
|
3125
3130
|
this.syncToolActiveFromWorkbench(event.id);
|
|
3126
3131
|
if (!this.isToolActive) {
|
|
3132
|
+
this.clearSnapGuides();
|
|
3127
3133
|
this.setImageFocus(null, {
|
|
3128
3134
|
syncCanvasSelection: true,
|
|
3129
3135
|
skipRender: true
|
|
@@ -3174,6 +3180,7 @@ var ImageTool = class {
|
|
|
3174
3180
|
this.updateImages();
|
|
3175
3181
|
};
|
|
3176
3182
|
this.onSelectionCleared = () => {
|
|
3183
|
+
this.clearSnapGuides();
|
|
3177
3184
|
this.setImageFocus(null, {
|
|
3178
3185
|
syncCanvasSelection: false,
|
|
3179
3186
|
skipRender: true
|
|
@@ -3182,6 +3189,7 @@ var ImageTool = class {
|
|
|
3182
3189
|
this.updateImages();
|
|
3183
3190
|
};
|
|
3184
3191
|
this.onSceneLayoutChanged = () => {
|
|
3192
|
+
this.updateSnapGuideVisuals();
|
|
3185
3193
|
this.updateImages();
|
|
3186
3194
|
};
|
|
3187
3195
|
this.onSceneGeometryChanged = () => {
|
|
@@ -3196,6 +3204,9 @@ var ImageTool = class {
|
|
|
3196
3204
|
if (typeof id !== "string" || layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
3197
3205
|
const frame = this.getFrameRect();
|
|
3198
3206
|
if (!frame.width || !frame.height) return;
|
|
3207
|
+
const matches = this.computeMoveSnapMatches(target, frame);
|
|
3208
|
+
this.applySnapMatchesToTarget(target, matches);
|
|
3209
|
+
this.clearSnapGuides();
|
|
3199
3210
|
const center = target.getCenterPoint ? target.getCenterPoint() : new import_fabric2.Point((_c = target.left) != null ? _c : 0, (_d = target.top) != null ? _d : 0);
|
|
3200
3211
|
const centerScene = this.canvasService ? this.canvasService.toScenePoint({ x: center.x, y: center.y }) : { x: center.x, y: center.y };
|
|
3201
3212
|
const objectScale = Number.isFinite(target == null ? void 0 : target.scaleX) ? target.scaleX : 1;
|
|
@@ -3258,8 +3269,17 @@ var ImageTool = class {
|
|
|
3258
3269
|
}),
|
|
3259
3270
|
{ priority: 300 }
|
|
3260
3271
|
);
|
|
3261
|
-
this.
|
|
3262
|
-
this.subscriptions.on(
|
|
3272
|
+
this.bindCanvasInteractionHandlers();
|
|
3273
|
+
this.subscriptions.on(
|
|
3274
|
+
context.eventBus,
|
|
3275
|
+
"tool:activated",
|
|
3276
|
+
this.onToolActivated
|
|
3277
|
+
);
|
|
3278
|
+
this.subscriptions.on(
|
|
3279
|
+
context.eventBus,
|
|
3280
|
+
"object:modified",
|
|
3281
|
+
this.onObjectModified
|
|
3282
|
+
);
|
|
3263
3283
|
this.subscriptions.on(
|
|
3264
3284
|
context.eventBus,
|
|
3265
3285
|
"selection:created",
|
|
@@ -3327,6 +3347,8 @@ var ImageTool = class {
|
|
|
3327
3347
|
this.imageSpecs = [];
|
|
3328
3348
|
this.overlaySpecs = [];
|
|
3329
3349
|
this.imageControlsByCapabilityKey.clear();
|
|
3350
|
+
this.clearSnapGuides();
|
|
3351
|
+
this.unbindCanvasInteractionHandlers();
|
|
3330
3352
|
this.clearRenderedImages();
|
|
3331
3353
|
(_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
|
|
3332
3354
|
this.renderProducerDisposable = void 0;
|
|
@@ -3336,6 +3358,266 @@ var ImageTool = class {
|
|
|
3336
3358
|
}
|
|
3337
3359
|
this.context = void 0;
|
|
3338
3360
|
}
|
|
3361
|
+
bindCanvasInteractionHandlers() {
|
|
3362
|
+
if (!this.canvasService || this.canvasObjectMovingHandler) return;
|
|
3363
|
+
this.canvasObjectMovingHandler = (e) => {
|
|
3364
|
+
this.handleCanvasObjectMoving(e);
|
|
3365
|
+
};
|
|
3366
|
+
this.canvasService.canvas.on(
|
|
3367
|
+
"object:moving",
|
|
3368
|
+
this.canvasObjectMovingHandler
|
|
3369
|
+
);
|
|
3370
|
+
}
|
|
3371
|
+
unbindCanvasInteractionHandlers() {
|
|
3372
|
+
if (!this.canvasService || !this.canvasObjectMovingHandler) return;
|
|
3373
|
+
this.canvasService.canvas.off(
|
|
3374
|
+
"object:moving",
|
|
3375
|
+
this.canvasObjectMovingHandler
|
|
3376
|
+
);
|
|
3377
|
+
this.canvasObjectMovingHandler = void 0;
|
|
3378
|
+
}
|
|
3379
|
+
getActiveImageTarget(target) {
|
|
3380
|
+
var _a, _b;
|
|
3381
|
+
if (!this.isToolActive) return null;
|
|
3382
|
+
if (!target) return null;
|
|
3383
|
+
if (((_a = target == null ? void 0 : target.data) == null ? void 0 : _a.layerId) !== IMAGE_OBJECT_LAYER_ID) return null;
|
|
3384
|
+
if (typeof ((_b = target == null ? void 0 : target.data) == null ? void 0 : _b.id) !== "string") return null;
|
|
3385
|
+
return target;
|
|
3386
|
+
}
|
|
3387
|
+
getTargetBoundsScene(target) {
|
|
3388
|
+
if (!this.canvasService || !target) return null;
|
|
3389
|
+
const rawBounds = typeof target.getBoundingRect === "function" ? target.getBoundingRect() : {
|
|
3390
|
+
left: Number(target.left || 0),
|
|
3391
|
+
top: Number(target.top || 0),
|
|
3392
|
+
width: Number(target.width || 0),
|
|
3393
|
+
height: Number(target.height || 0)
|
|
3394
|
+
};
|
|
3395
|
+
return this.canvasService.toSceneRect({
|
|
3396
|
+
left: Number(rawBounds.left || 0),
|
|
3397
|
+
top: Number(rawBounds.top || 0),
|
|
3398
|
+
width: Number(rawBounds.width || 0),
|
|
3399
|
+
height: Number(rawBounds.height || 0)
|
|
3400
|
+
});
|
|
3401
|
+
}
|
|
3402
|
+
getSnapThresholdScene(px) {
|
|
3403
|
+
if (!this.canvasService) return px;
|
|
3404
|
+
return this.canvasService.toSceneLength(px);
|
|
3405
|
+
}
|
|
3406
|
+
pickSnapMatch(candidates, previous) {
|
|
3407
|
+
if (!candidates.length) return null;
|
|
3408
|
+
const snapThreshold = this.getSnapThresholdScene(
|
|
3409
|
+
IMAGE_MOVE_SNAP_THRESHOLD_PX
|
|
3410
|
+
);
|
|
3411
|
+
const releaseThreshold = this.getSnapThresholdScene(
|
|
3412
|
+
IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX
|
|
3413
|
+
);
|
|
3414
|
+
if (previous) {
|
|
3415
|
+
const sticky = candidates.find((candidate) => {
|
|
3416
|
+
return candidate.lineId === previous.lineId && Math.abs(candidate.deltaScene) <= releaseThreshold;
|
|
3417
|
+
});
|
|
3418
|
+
if (sticky) return sticky;
|
|
3419
|
+
}
|
|
3420
|
+
let best = null;
|
|
3421
|
+
candidates.forEach((candidate) => {
|
|
3422
|
+
if (Math.abs(candidate.deltaScene) > snapThreshold) return;
|
|
3423
|
+
if (!best || Math.abs(candidate.deltaScene) < Math.abs(best.deltaScene)) {
|
|
3424
|
+
best = candidate;
|
|
3425
|
+
}
|
|
3426
|
+
});
|
|
3427
|
+
return best;
|
|
3428
|
+
}
|
|
3429
|
+
computeMoveSnapMatches(target, frame) {
|
|
3430
|
+
const bounds = this.getTargetBoundsScene(target);
|
|
3431
|
+
if (!bounds || frame.width <= 0 || frame.height <= 0) {
|
|
3432
|
+
return { x: null, y: null };
|
|
3433
|
+
}
|
|
3434
|
+
const xCandidates = [
|
|
3435
|
+
{
|
|
3436
|
+
axis: "x",
|
|
3437
|
+
lineId: "frame-left",
|
|
3438
|
+
kind: "edge",
|
|
3439
|
+
lineScene: frame.left,
|
|
3440
|
+
deltaScene: frame.left - bounds.left
|
|
3441
|
+
},
|
|
3442
|
+
{
|
|
3443
|
+
axis: "x",
|
|
3444
|
+
lineId: "frame-center-x",
|
|
3445
|
+
kind: "center",
|
|
3446
|
+
lineScene: frame.left + frame.width / 2,
|
|
3447
|
+
deltaScene: frame.left + frame.width / 2 - (bounds.left + bounds.width / 2)
|
|
3448
|
+
},
|
|
3449
|
+
{
|
|
3450
|
+
axis: "x",
|
|
3451
|
+
lineId: "frame-right",
|
|
3452
|
+
kind: "edge",
|
|
3453
|
+
lineScene: frame.left + frame.width,
|
|
3454
|
+
deltaScene: frame.left + frame.width - (bounds.left + bounds.width)
|
|
3455
|
+
}
|
|
3456
|
+
];
|
|
3457
|
+
const yCandidates = [
|
|
3458
|
+
{
|
|
3459
|
+
axis: "y",
|
|
3460
|
+
lineId: "frame-top",
|
|
3461
|
+
kind: "edge",
|
|
3462
|
+
lineScene: frame.top,
|
|
3463
|
+
deltaScene: frame.top - bounds.top
|
|
3464
|
+
},
|
|
3465
|
+
{
|
|
3466
|
+
axis: "y",
|
|
3467
|
+
lineId: "frame-center-y",
|
|
3468
|
+
kind: "center",
|
|
3469
|
+
lineScene: frame.top + frame.height / 2,
|
|
3470
|
+
deltaScene: frame.top + frame.height / 2 - (bounds.top + bounds.height / 2)
|
|
3471
|
+
},
|
|
3472
|
+
{
|
|
3473
|
+
axis: "y",
|
|
3474
|
+
lineId: "frame-bottom",
|
|
3475
|
+
kind: "edge",
|
|
3476
|
+
lineScene: frame.top + frame.height,
|
|
3477
|
+
deltaScene: frame.top + frame.height - (bounds.top + bounds.height)
|
|
3478
|
+
}
|
|
3479
|
+
];
|
|
3480
|
+
return {
|
|
3481
|
+
x: this.pickSnapMatch(xCandidates, this.activeSnapX),
|
|
3482
|
+
y: this.pickSnapMatch(yCandidates, this.activeSnapY)
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
areSnapMatchesEqual(a, b) {
|
|
3486
|
+
if (!a && !b) return true;
|
|
3487
|
+
if (!a || !b) return false;
|
|
3488
|
+
return a.lineId === b.lineId && a.axis === b.axis && a.kind === b.kind;
|
|
3489
|
+
}
|
|
3490
|
+
updateSnapMatchState(nextX, nextY) {
|
|
3491
|
+
const changed = !this.areSnapMatchesEqual(this.activeSnapX, nextX) || !this.areSnapMatchesEqual(this.activeSnapY, nextY);
|
|
3492
|
+
this.activeSnapX = nextX;
|
|
3493
|
+
this.activeSnapY = nextY;
|
|
3494
|
+
if (changed) {
|
|
3495
|
+
this.updateSnapGuideVisuals();
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
clearSnapGuides() {
|
|
3499
|
+
var _a;
|
|
3500
|
+
this.activeSnapX = null;
|
|
3501
|
+
this.activeSnapY = null;
|
|
3502
|
+
this.removeSnapGuideObject("x");
|
|
3503
|
+
this.removeSnapGuideObject("y");
|
|
3504
|
+
(_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
|
|
3505
|
+
}
|
|
3506
|
+
removeSnapGuideObject(axis) {
|
|
3507
|
+
if (!this.canvasService) return;
|
|
3508
|
+
const canvas = this.canvasService.canvas;
|
|
3509
|
+
const current = axis === "x" ? this.snapGuideXObject : this.snapGuideYObject;
|
|
3510
|
+
if (!current) return;
|
|
3511
|
+
canvas.remove(current);
|
|
3512
|
+
if (axis === "x") {
|
|
3513
|
+
this.snapGuideXObject = void 0;
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
this.snapGuideYObject = void 0;
|
|
3517
|
+
}
|
|
3518
|
+
createOrUpdateSnapGuideObject(axis, pathData) {
|
|
3519
|
+
if (!this.canvasService) return;
|
|
3520
|
+
const canvas = this.canvasService.canvas;
|
|
3521
|
+
const color = this.getConfig("image.control.borderColor", "#1677ff") || "#1677ff";
|
|
3522
|
+
const strokeWidth = 1;
|
|
3523
|
+
this.removeSnapGuideObject(axis);
|
|
3524
|
+
const created = new import_fabric2.Path(pathData, {
|
|
3525
|
+
originX: "left",
|
|
3526
|
+
originY: "top",
|
|
3527
|
+
fill: "rgba(0,0,0,0)",
|
|
3528
|
+
stroke: color,
|
|
3529
|
+
strokeWidth,
|
|
3530
|
+
selectable: false,
|
|
3531
|
+
evented: false,
|
|
3532
|
+
excludeFromExport: true,
|
|
3533
|
+
objectCaching: false,
|
|
3534
|
+
data: {
|
|
3535
|
+
id: `${IMAGE_SNAP_GUIDE_LAYER_ID}.${axis}`,
|
|
3536
|
+
layerId: IMAGE_SNAP_GUIDE_LAYER_ID,
|
|
3537
|
+
type: "image-snap-guide"
|
|
3538
|
+
}
|
|
3539
|
+
});
|
|
3540
|
+
created.setCoords();
|
|
3541
|
+
canvas.add(created);
|
|
3542
|
+
canvas.bringObjectToFront(created);
|
|
3543
|
+
if (axis === "x") {
|
|
3544
|
+
this.snapGuideXObject = created;
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
this.snapGuideYObject = created;
|
|
3548
|
+
}
|
|
3549
|
+
updateSnapGuideVisuals() {
|
|
3550
|
+
if (!this.canvasService || !this.isImageEditingVisible()) {
|
|
3551
|
+
this.removeSnapGuideObject("x");
|
|
3552
|
+
this.removeSnapGuideObject("y");
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
const frame = this.getFrameRect();
|
|
3556
|
+
if (frame.width <= 0 || frame.height <= 0) {
|
|
3557
|
+
this.removeSnapGuideObject("x");
|
|
3558
|
+
this.removeSnapGuideObject("y");
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3561
|
+
const frameScreen = this.getFrameRectScreen(frame);
|
|
3562
|
+
if (this.activeSnapX) {
|
|
3563
|
+
const x = this.canvasService.toScreenPoint({
|
|
3564
|
+
x: this.activeSnapX.lineScene,
|
|
3565
|
+
y: frame.top
|
|
3566
|
+
}).x;
|
|
3567
|
+
this.createOrUpdateSnapGuideObject(
|
|
3568
|
+
"x",
|
|
3569
|
+
`M ${x} ${frameScreen.top} L ${x} ${frameScreen.top + frameScreen.height}`
|
|
3570
|
+
);
|
|
3571
|
+
} else {
|
|
3572
|
+
this.removeSnapGuideObject("x");
|
|
3573
|
+
}
|
|
3574
|
+
if (this.activeSnapY) {
|
|
3575
|
+
const y = this.canvasService.toScreenPoint({
|
|
3576
|
+
x: frame.left,
|
|
3577
|
+
y: this.activeSnapY.lineScene
|
|
3578
|
+
}).y;
|
|
3579
|
+
this.createOrUpdateSnapGuideObject(
|
|
3580
|
+
"y",
|
|
3581
|
+
`M ${frameScreen.left} ${y} L ${frameScreen.left + frameScreen.width} ${y}`
|
|
3582
|
+
);
|
|
3583
|
+
} else {
|
|
3584
|
+
this.removeSnapGuideObject("y");
|
|
3585
|
+
}
|
|
3586
|
+
this.canvasService.requestRenderAll();
|
|
3587
|
+
}
|
|
3588
|
+
handleCanvasObjectMoving(e) {
|
|
3589
|
+
var _a, _b, _c, _d;
|
|
3590
|
+
const target = this.getActiveImageTarget(e == null ? void 0 : e.target);
|
|
3591
|
+
if (!target || !this.canvasService) return;
|
|
3592
|
+
const frame = this.getFrameRect();
|
|
3593
|
+
if (frame.width <= 0 || frame.height <= 0) {
|
|
3594
|
+
this.clearSnapGuides();
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
const matches = this.computeMoveSnapMatches(target, frame);
|
|
3598
|
+
const deltaX = (_b = (_a = matches.x) == null ? void 0 : _a.deltaScene) != null ? _b : 0;
|
|
3599
|
+
const deltaY = (_d = (_c = matches.y) == null ? void 0 : _c.deltaScene) != null ? _d : 0;
|
|
3600
|
+
if (deltaX || deltaY) {
|
|
3601
|
+
target.set({
|
|
3602
|
+
left: Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
|
|
3603
|
+
top: Number(target.top || 0) + this.canvasService.toScreenLength(deltaY)
|
|
3604
|
+
});
|
|
3605
|
+
target.setCoords();
|
|
3606
|
+
}
|
|
3607
|
+
this.updateSnapMatchState(matches.x, matches.y);
|
|
3608
|
+
}
|
|
3609
|
+
applySnapMatchesToTarget(target, matches) {
|
|
3610
|
+
var _a, _b, _c, _d;
|
|
3611
|
+
if (!this.canvasService || !target) return;
|
|
3612
|
+
const deltaX = (_b = (_a = matches.x) == null ? void 0 : _a.deltaScene) != null ? _b : 0;
|
|
3613
|
+
const deltaY = (_d = (_c = matches.y) == null ? void 0 : _c.deltaScene) != null ? _d : 0;
|
|
3614
|
+
if (!deltaX && !deltaY) return;
|
|
3615
|
+
target.set({
|
|
3616
|
+
left: Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
|
|
3617
|
+
top: Number(target.top || 0) + this.canvasService.toScreenLength(deltaY)
|
|
3618
|
+
});
|
|
3619
|
+
target.setCoords();
|
|
3620
|
+
}
|
|
3339
3621
|
syncToolActiveFromWorkbench(fallbackId) {
|
|
3340
3622
|
var _a;
|
|
3341
3623
|
const wb = (_a = this.context) == null ? void 0 : _a.services.get("WorkbenchService");
|
|
@@ -4322,6 +4604,7 @@ var ImageTool = class {
|
|
|
4322
4604
|
isImageSelectionActive: this.isImageSelectionActive,
|
|
4323
4605
|
focusedImageId: this.focusedImageId
|
|
4324
4606
|
});
|
|
4607
|
+
this.updateSnapGuideVisuals();
|
|
4325
4608
|
this.canvasService.requestRenderAll();
|
|
4326
4609
|
}
|
|
4327
4610
|
clampNormalized(value) {
|
package/dist/index.mjs
CHANGED
|
@@ -1025,6 +1025,7 @@ import {
|
|
|
1025
1025
|
Canvas as FabricCanvas,
|
|
1026
1026
|
Control,
|
|
1027
1027
|
Image as FabricImage2,
|
|
1028
|
+
Path as FabricPath,
|
|
1028
1029
|
Pattern,
|
|
1029
1030
|
Point,
|
|
1030
1031
|
controlsUtils
|
|
@@ -1998,6 +1999,9 @@ var IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
|
|
|
1998
1999
|
"rotate",
|
|
1999
2000
|
"scale"
|
|
2000
2001
|
];
|
|
2002
|
+
var IMAGE_MOVE_SNAP_THRESHOLD_PX = 6;
|
|
2003
|
+
var IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX = 10;
|
|
2004
|
+
var IMAGE_SNAP_GUIDE_LAYER_ID = "image.snapGuide";
|
|
2001
2005
|
var IMAGE_CONTROL_DESCRIPTORS = [
|
|
2002
2006
|
{
|
|
2003
2007
|
key: "tl",
|
|
@@ -2042,12 +2046,15 @@ var ImageTool = class {
|
|
|
2042
2046
|
this.renderSeq = 0;
|
|
2043
2047
|
this.imageSpecs = [];
|
|
2044
2048
|
this.overlaySpecs = [];
|
|
2049
|
+
this.activeSnapX = null;
|
|
2050
|
+
this.activeSnapY = null;
|
|
2045
2051
|
this.subscriptions = new SubscriptionBag();
|
|
2046
2052
|
this.imageControlsByCapabilityKey = /* @__PURE__ */ new Map();
|
|
2047
2053
|
this.onToolActivated = (event) => {
|
|
2048
2054
|
const before = this.isToolActive;
|
|
2049
2055
|
this.syncToolActiveFromWorkbench(event.id);
|
|
2050
2056
|
if (!this.isToolActive) {
|
|
2057
|
+
this.clearSnapGuides();
|
|
2051
2058
|
this.setImageFocus(null, {
|
|
2052
2059
|
syncCanvasSelection: true,
|
|
2053
2060
|
skipRender: true
|
|
@@ -2098,6 +2105,7 @@ var ImageTool = class {
|
|
|
2098
2105
|
this.updateImages();
|
|
2099
2106
|
};
|
|
2100
2107
|
this.onSelectionCleared = () => {
|
|
2108
|
+
this.clearSnapGuides();
|
|
2101
2109
|
this.setImageFocus(null, {
|
|
2102
2110
|
syncCanvasSelection: false,
|
|
2103
2111
|
skipRender: true
|
|
@@ -2106,6 +2114,7 @@ var ImageTool = class {
|
|
|
2106
2114
|
this.updateImages();
|
|
2107
2115
|
};
|
|
2108
2116
|
this.onSceneLayoutChanged = () => {
|
|
2117
|
+
this.updateSnapGuideVisuals();
|
|
2109
2118
|
this.updateImages();
|
|
2110
2119
|
};
|
|
2111
2120
|
this.onSceneGeometryChanged = () => {
|
|
@@ -2120,6 +2129,9 @@ var ImageTool = class {
|
|
|
2120
2129
|
if (typeof id !== "string" || layerId !== IMAGE_OBJECT_LAYER_ID) return;
|
|
2121
2130
|
const frame = this.getFrameRect();
|
|
2122
2131
|
if (!frame.width || !frame.height) return;
|
|
2132
|
+
const matches = this.computeMoveSnapMatches(target, frame);
|
|
2133
|
+
this.applySnapMatchesToTarget(target, matches);
|
|
2134
|
+
this.clearSnapGuides();
|
|
2123
2135
|
const center = target.getCenterPoint ? target.getCenterPoint() : new Point((_c = target.left) != null ? _c : 0, (_d = target.top) != null ? _d : 0);
|
|
2124
2136
|
const centerScene = this.canvasService ? this.canvasService.toScenePoint({ x: center.x, y: center.y }) : { x: center.x, y: center.y };
|
|
2125
2137
|
const objectScale = Number.isFinite(target == null ? void 0 : target.scaleX) ? target.scaleX : 1;
|
|
@@ -2182,8 +2194,17 @@ var ImageTool = class {
|
|
|
2182
2194
|
}),
|
|
2183
2195
|
{ priority: 300 }
|
|
2184
2196
|
);
|
|
2185
|
-
this.
|
|
2186
|
-
this.subscriptions.on(
|
|
2197
|
+
this.bindCanvasInteractionHandlers();
|
|
2198
|
+
this.subscriptions.on(
|
|
2199
|
+
context.eventBus,
|
|
2200
|
+
"tool:activated",
|
|
2201
|
+
this.onToolActivated
|
|
2202
|
+
);
|
|
2203
|
+
this.subscriptions.on(
|
|
2204
|
+
context.eventBus,
|
|
2205
|
+
"object:modified",
|
|
2206
|
+
this.onObjectModified
|
|
2207
|
+
);
|
|
2187
2208
|
this.subscriptions.on(
|
|
2188
2209
|
context.eventBus,
|
|
2189
2210
|
"selection:created",
|
|
@@ -2251,6 +2272,8 @@ var ImageTool = class {
|
|
|
2251
2272
|
this.imageSpecs = [];
|
|
2252
2273
|
this.overlaySpecs = [];
|
|
2253
2274
|
this.imageControlsByCapabilityKey.clear();
|
|
2275
|
+
this.clearSnapGuides();
|
|
2276
|
+
this.unbindCanvasInteractionHandlers();
|
|
2254
2277
|
this.clearRenderedImages();
|
|
2255
2278
|
(_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
|
|
2256
2279
|
this.renderProducerDisposable = void 0;
|
|
@@ -2260,6 +2283,266 @@ var ImageTool = class {
|
|
|
2260
2283
|
}
|
|
2261
2284
|
this.context = void 0;
|
|
2262
2285
|
}
|
|
2286
|
+
bindCanvasInteractionHandlers() {
|
|
2287
|
+
if (!this.canvasService || this.canvasObjectMovingHandler) return;
|
|
2288
|
+
this.canvasObjectMovingHandler = (e) => {
|
|
2289
|
+
this.handleCanvasObjectMoving(e);
|
|
2290
|
+
};
|
|
2291
|
+
this.canvasService.canvas.on(
|
|
2292
|
+
"object:moving",
|
|
2293
|
+
this.canvasObjectMovingHandler
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
unbindCanvasInteractionHandlers() {
|
|
2297
|
+
if (!this.canvasService || !this.canvasObjectMovingHandler) return;
|
|
2298
|
+
this.canvasService.canvas.off(
|
|
2299
|
+
"object:moving",
|
|
2300
|
+
this.canvasObjectMovingHandler
|
|
2301
|
+
);
|
|
2302
|
+
this.canvasObjectMovingHandler = void 0;
|
|
2303
|
+
}
|
|
2304
|
+
getActiveImageTarget(target) {
|
|
2305
|
+
var _a, _b;
|
|
2306
|
+
if (!this.isToolActive) return null;
|
|
2307
|
+
if (!target) return null;
|
|
2308
|
+
if (((_a = target == null ? void 0 : target.data) == null ? void 0 : _a.layerId) !== IMAGE_OBJECT_LAYER_ID) return null;
|
|
2309
|
+
if (typeof ((_b = target == null ? void 0 : target.data) == null ? void 0 : _b.id) !== "string") return null;
|
|
2310
|
+
return target;
|
|
2311
|
+
}
|
|
2312
|
+
getTargetBoundsScene(target) {
|
|
2313
|
+
if (!this.canvasService || !target) return null;
|
|
2314
|
+
const rawBounds = typeof target.getBoundingRect === "function" ? target.getBoundingRect() : {
|
|
2315
|
+
left: Number(target.left || 0),
|
|
2316
|
+
top: Number(target.top || 0),
|
|
2317
|
+
width: Number(target.width || 0),
|
|
2318
|
+
height: Number(target.height || 0)
|
|
2319
|
+
};
|
|
2320
|
+
return this.canvasService.toSceneRect({
|
|
2321
|
+
left: Number(rawBounds.left || 0),
|
|
2322
|
+
top: Number(rawBounds.top || 0),
|
|
2323
|
+
width: Number(rawBounds.width || 0),
|
|
2324
|
+
height: Number(rawBounds.height || 0)
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
getSnapThresholdScene(px) {
|
|
2328
|
+
if (!this.canvasService) return px;
|
|
2329
|
+
return this.canvasService.toSceneLength(px);
|
|
2330
|
+
}
|
|
2331
|
+
pickSnapMatch(candidates, previous) {
|
|
2332
|
+
if (!candidates.length) return null;
|
|
2333
|
+
const snapThreshold = this.getSnapThresholdScene(
|
|
2334
|
+
IMAGE_MOVE_SNAP_THRESHOLD_PX
|
|
2335
|
+
);
|
|
2336
|
+
const releaseThreshold = this.getSnapThresholdScene(
|
|
2337
|
+
IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX
|
|
2338
|
+
);
|
|
2339
|
+
if (previous) {
|
|
2340
|
+
const sticky = candidates.find((candidate) => {
|
|
2341
|
+
return candidate.lineId === previous.lineId && Math.abs(candidate.deltaScene) <= releaseThreshold;
|
|
2342
|
+
});
|
|
2343
|
+
if (sticky) return sticky;
|
|
2344
|
+
}
|
|
2345
|
+
let best = null;
|
|
2346
|
+
candidates.forEach((candidate) => {
|
|
2347
|
+
if (Math.abs(candidate.deltaScene) > snapThreshold) return;
|
|
2348
|
+
if (!best || Math.abs(candidate.deltaScene) < Math.abs(best.deltaScene)) {
|
|
2349
|
+
best = candidate;
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
return best;
|
|
2353
|
+
}
|
|
2354
|
+
computeMoveSnapMatches(target, frame) {
|
|
2355
|
+
const bounds = this.getTargetBoundsScene(target);
|
|
2356
|
+
if (!bounds || frame.width <= 0 || frame.height <= 0) {
|
|
2357
|
+
return { x: null, y: null };
|
|
2358
|
+
}
|
|
2359
|
+
const xCandidates = [
|
|
2360
|
+
{
|
|
2361
|
+
axis: "x",
|
|
2362
|
+
lineId: "frame-left",
|
|
2363
|
+
kind: "edge",
|
|
2364
|
+
lineScene: frame.left,
|
|
2365
|
+
deltaScene: frame.left - bounds.left
|
|
2366
|
+
},
|
|
2367
|
+
{
|
|
2368
|
+
axis: "x",
|
|
2369
|
+
lineId: "frame-center-x",
|
|
2370
|
+
kind: "center",
|
|
2371
|
+
lineScene: frame.left + frame.width / 2,
|
|
2372
|
+
deltaScene: frame.left + frame.width / 2 - (bounds.left + bounds.width / 2)
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
axis: "x",
|
|
2376
|
+
lineId: "frame-right",
|
|
2377
|
+
kind: "edge",
|
|
2378
|
+
lineScene: frame.left + frame.width,
|
|
2379
|
+
deltaScene: frame.left + frame.width - (bounds.left + bounds.width)
|
|
2380
|
+
}
|
|
2381
|
+
];
|
|
2382
|
+
const yCandidates = [
|
|
2383
|
+
{
|
|
2384
|
+
axis: "y",
|
|
2385
|
+
lineId: "frame-top",
|
|
2386
|
+
kind: "edge",
|
|
2387
|
+
lineScene: frame.top,
|
|
2388
|
+
deltaScene: frame.top - bounds.top
|
|
2389
|
+
},
|
|
2390
|
+
{
|
|
2391
|
+
axis: "y",
|
|
2392
|
+
lineId: "frame-center-y",
|
|
2393
|
+
kind: "center",
|
|
2394
|
+
lineScene: frame.top + frame.height / 2,
|
|
2395
|
+
deltaScene: frame.top + frame.height / 2 - (bounds.top + bounds.height / 2)
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
axis: "y",
|
|
2399
|
+
lineId: "frame-bottom",
|
|
2400
|
+
kind: "edge",
|
|
2401
|
+
lineScene: frame.top + frame.height,
|
|
2402
|
+
deltaScene: frame.top + frame.height - (bounds.top + bounds.height)
|
|
2403
|
+
}
|
|
2404
|
+
];
|
|
2405
|
+
return {
|
|
2406
|
+
x: this.pickSnapMatch(xCandidates, this.activeSnapX),
|
|
2407
|
+
y: this.pickSnapMatch(yCandidates, this.activeSnapY)
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
areSnapMatchesEqual(a, b) {
|
|
2411
|
+
if (!a && !b) return true;
|
|
2412
|
+
if (!a || !b) return false;
|
|
2413
|
+
return a.lineId === b.lineId && a.axis === b.axis && a.kind === b.kind;
|
|
2414
|
+
}
|
|
2415
|
+
updateSnapMatchState(nextX, nextY) {
|
|
2416
|
+
const changed = !this.areSnapMatchesEqual(this.activeSnapX, nextX) || !this.areSnapMatchesEqual(this.activeSnapY, nextY);
|
|
2417
|
+
this.activeSnapX = nextX;
|
|
2418
|
+
this.activeSnapY = nextY;
|
|
2419
|
+
if (changed) {
|
|
2420
|
+
this.updateSnapGuideVisuals();
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
clearSnapGuides() {
|
|
2424
|
+
var _a;
|
|
2425
|
+
this.activeSnapX = null;
|
|
2426
|
+
this.activeSnapY = null;
|
|
2427
|
+
this.removeSnapGuideObject("x");
|
|
2428
|
+
this.removeSnapGuideObject("y");
|
|
2429
|
+
(_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
|
|
2430
|
+
}
|
|
2431
|
+
removeSnapGuideObject(axis) {
|
|
2432
|
+
if (!this.canvasService) return;
|
|
2433
|
+
const canvas = this.canvasService.canvas;
|
|
2434
|
+
const current = axis === "x" ? this.snapGuideXObject : this.snapGuideYObject;
|
|
2435
|
+
if (!current) return;
|
|
2436
|
+
canvas.remove(current);
|
|
2437
|
+
if (axis === "x") {
|
|
2438
|
+
this.snapGuideXObject = void 0;
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
this.snapGuideYObject = void 0;
|
|
2442
|
+
}
|
|
2443
|
+
createOrUpdateSnapGuideObject(axis, pathData) {
|
|
2444
|
+
if (!this.canvasService) return;
|
|
2445
|
+
const canvas = this.canvasService.canvas;
|
|
2446
|
+
const color = this.getConfig("image.control.borderColor", "#1677ff") || "#1677ff";
|
|
2447
|
+
const strokeWidth = 1;
|
|
2448
|
+
this.removeSnapGuideObject(axis);
|
|
2449
|
+
const created = new FabricPath(pathData, {
|
|
2450
|
+
originX: "left",
|
|
2451
|
+
originY: "top",
|
|
2452
|
+
fill: "rgba(0,0,0,0)",
|
|
2453
|
+
stroke: color,
|
|
2454
|
+
strokeWidth,
|
|
2455
|
+
selectable: false,
|
|
2456
|
+
evented: false,
|
|
2457
|
+
excludeFromExport: true,
|
|
2458
|
+
objectCaching: false,
|
|
2459
|
+
data: {
|
|
2460
|
+
id: `${IMAGE_SNAP_GUIDE_LAYER_ID}.${axis}`,
|
|
2461
|
+
layerId: IMAGE_SNAP_GUIDE_LAYER_ID,
|
|
2462
|
+
type: "image-snap-guide"
|
|
2463
|
+
}
|
|
2464
|
+
});
|
|
2465
|
+
created.setCoords();
|
|
2466
|
+
canvas.add(created);
|
|
2467
|
+
canvas.bringObjectToFront(created);
|
|
2468
|
+
if (axis === "x") {
|
|
2469
|
+
this.snapGuideXObject = created;
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
this.snapGuideYObject = created;
|
|
2473
|
+
}
|
|
2474
|
+
updateSnapGuideVisuals() {
|
|
2475
|
+
if (!this.canvasService || !this.isImageEditingVisible()) {
|
|
2476
|
+
this.removeSnapGuideObject("x");
|
|
2477
|
+
this.removeSnapGuideObject("y");
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
const frame = this.getFrameRect();
|
|
2481
|
+
if (frame.width <= 0 || frame.height <= 0) {
|
|
2482
|
+
this.removeSnapGuideObject("x");
|
|
2483
|
+
this.removeSnapGuideObject("y");
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
const frameScreen = this.getFrameRectScreen(frame);
|
|
2487
|
+
if (this.activeSnapX) {
|
|
2488
|
+
const x = this.canvasService.toScreenPoint({
|
|
2489
|
+
x: this.activeSnapX.lineScene,
|
|
2490
|
+
y: frame.top
|
|
2491
|
+
}).x;
|
|
2492
|
+
this.createOrUpdateSnapGuideObject(
|
|
2493
|
+
"x",
|
|
2494
|
+
`M ${x} ${frameScreen.top} L ${x} ${frameScreen.top + frameScreen.height}`
|
|
2495
|
+
);
|
|
2496
|
+
} else {
|
|
2497
|
+
this.removeSnapGuideObject("x");
|
|
2498
|
+
}
|
|
2499
|
+
if (this.activeSnapY) {
|
|
2500
|
+
const y = this.canvasService.toScreenPoint({
|
|
2501
|
+
x: frame.left,
|
|
2502
|
+
y: this.activeSnapY.lineScene
|
|
2503
|
+
}).y;
|
|
2504
|
+
this.createOrUpdateSnapGuideObject(
|
|
2505
|
+
"y",
|
|
2506
|
+
`M ${frameScreen.left} ${y} L ${frameScreen.left + frameScreen.width} ${y}`
|
|
2507
|
+
);
|
|
2508
|
+
} else {
|
|
2509
|
+
this.removeSnapGuideObject("y");
|
|
2510
|
+
}
|
|
2511
|
+
this.canvasService.requestRenderAll();
|
|
2512
|
+
}
|
|
2513
|
+
handleCanvasObjectMoving(e) {
|
|
2514
|
+
var _a, _b, _c, _d;
|
|
2515
|
+
const target = this.getActiveImageTarget(e == null ? void 0 : e.target);
|
|
2516
|
+
if (!target || !this.canvasService) return;
|
|
2517
|
+
const frame = this.getFrameRect();
|
|
2518
|
+
if (frame.width <= 0 || frame.height <= 0) {
|
|
2519
|
+
this.clearSnapGuides();
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
const matches = this.computeMoveSnapMatches(target, frame);
|
|
2523
|
+
const deltaX = (_b = (_a = matches.x) == null ? void 0 : _a.deltaScene) != null ? _b : 0;
|
|
2524
|
+
const deltaY = (_d = (_c = matches.y) == null ? void 0 : _c.deltaScene) != null ? _d : 0;
|
|
2525
|
+
if (deltaX || deltaY) {
|
|
2526
|
+
target.set({
|
|
2527
|
+
left: Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
|
|
2528
|
+
top: Number(target.top || 0) + this.canvasService.toScreenLength(deltaY)
|
|
2529
|
+
});
|
|
2530
|
+
target.setCoords();
|
|
2531
|
+
}
|
|
2532
|
+
this.updateSnapMatchState(matches.x, matches.y);
|
|
2533
|
+
}
|
|
2534
|
+
applySnapMatchesToTarget(target, matches) {
|
|
2535
|
+
var _a, _b, _c, _d;
|
|
2536
|
+
if (!this.canvasService || !target) return;
|
|
2537
|
+
const deltaX = (_b = (_a = matches.x) == null ? void 0 : _a.deltaScene) != null ? _b : 0;
|
|
2538
|
+
const deltaY = (_d = (_c = matches.y) == null ? void 0 : _c.deltaScene) != null ? _d : 0;
|
|
2539
|
+
if (!deltaX && !deltaY) return;
|
|
2540
|
+
target.set({
|
|
2541
|
+
left: Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
|
|
2542
|
+
top: Number(target.top || 0) + this.canvasService.toScreenLength(deltaY)
|
|
2543
|
+
});
|
|
2544
|
+
target.setCoords();
|
|
2545
|
+
}
|
|
2263
2546
|
syncToolActiveFromWorkbench(fallbackId) {
|
|
2264
2547
|
var _a;
|
|
2265
2548
|
const wb = (_a = this.context) == null ? void 0 : _a.services.get("WorkbenchService");
|
|
@@ -3246,6 +3529,7 @@ var ImageTool = class {
|
|
|
3246
3529
|
isImageSelectionActive: this.isImageSelectionActive,
|
|
3247
3530
|
focusedImageId: this.focusedImageId
|
|
3248
3531
|
});
|
|
3532
|
+
this.updateSnapGuideVisuals();
|
|
3249
3533
|
this.canvasService.requestRenderAll();
|
|
3250
3534
|
}
|
|
3251
3535
|
clampNormalized(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pooder/kit",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.2",
|
|
4
4
|
"description": "Standard plugins for Pooder editor",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"paper": "^0.12.18",
|
|
21
21
|
"fabric": "^7.0.0",
|
|
22
|
-
"@pooder/core": "2.2.
|
|
22
|
+
"@pooder/core": "2.2.1"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
@@ -10,11 +10,16 @@ import {
|
|
|
10
10
|
Canvas as FabricCanvas,
|
|
11
11
|
Control,
|
|
12
12
|
Image as FabricImage,
|
|
13
|
+
Path as FabricPath,
|
|
13
14
|
Pattern,
|
|
14
15
|
Point,
|
|
15
16
|
controlsUtils,
|
|
16
17
|
} from "fabric";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
CanvasService,
|
|
20
|
+
RenderLayoutRect,
|
|
21
|
+
RenderObjectSpec,
|
|
22
|
+
} from "../../services";
|
|
18
23
|
import { isDielineShape, normalizeShapeStyle } from "../dielineShape";
|
|
19
24
|
import type { DielineShape, DielineShapeStyle } from "../dielineShape";
|
|
20
25
|
import { generateDielinePath, getPathBounds } from "../geometry";
|
|
@@ -140,11 +145,41 @@ interface ImageControlDescriptor {
|
|
|
140
145
|
create: () => Control;
|
|
141
146
|
}
|
|
142
147
|
|
|
148
|
+
type SnapAxis = "x" | "y";
|
|
149
|
+
type SnapLineKind = "edge" | "center";
|
|
150
|
+
type SnapLineId =
|
|
151
|
+
| "frame-left"
|
|
152
|
+
| "frame-center-x"
|
|
153
|
+
| "frame-right"
|
|
154
|
+
| "frame-top"
|
|
155
|
+
| "frame-center-y"
|
|
156
|
+
| "frame-bottom";
|
|
157
|
+
|
|
158
|
+
interface SnapMatch {
|
|
159
|
+
axis: SnapAxis;
|
|
160
|
+
lineId: SnapLineId;
|
|
161
|
+
kind: SnapLineKind;
|
|
162
|
+
lineScene: number;
|
|
163
|
+
deltaScene: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface SnapCandidate {
|
|
167
|
+
axis: SnapAxis;
|
|
168
|
+
lineId: SnapLineId;
|
|
169
|
+
kind: SnapLineKind;
|
|
170
|
+
lineScene: number;
|
|
171
|
+
deltaScene: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
143
174
|
const IMAGE_DEFAULT_CONTROL_CAPABILITIES: ImageControlCapability[] = [
|
|
144
175
|
"rotate",
|
|
145
176
|
"scale",
|
|
146
177
|
];
|
|
147
178
|
|
|
179
|
+
const IMAGE_MOVE_SNAP_THRESHOLD_PX = 6;
|
|
180
|
+
const IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX = 10;
|
|
181
|
+
const IMAGE_SNAP_GUIDE_LAYER_ID = "image.snapGuide";
|
|
182
|
+
|
|
148
183
|
const IMAGE_CONTROL_DESCRIPTORS: ImageControlDescriptor[] = [
|
|
149
184
|
{
|
|
150
185
|
key: "tl",
|
|
@@ -199,6 +234,11 @@ export class ImageTool implements Extension {
|
|
|
199
234
|
private cropShapeHatchPatternKey?: string;
|
|
200
235
|
private imageSpecs: RenderObjectSpec[] = [];
|
|
201
236
|
private overlaySpecs: RenderObjectSpec[] = [];
|
|
237
|
+
private activeSnapX: SnapMatch | null = null;
|
|
238
|
+
private activeSnapY: SnapMatch | null = null;
|
|
239
|
+
private snapGuideXObject?: FabricPath;
|
|
240
|
+
private snapGuideYObject?: FabricPath;
|
|
241
|
+
private canvasObjectMovingHandler?: (e: any) => void;
|
|
202
242
|
private renderProducerDisposable?: { dispose: () => void };
|
|
203
243
|
private readonly subscriptions = new SubscriptionBag();
|
|
204
244
|
private imageControlsByCapabilityKey: Map<string, Record<string, Control>> =
|
|
@@ -247,9 +287,18 @@ export class ImageTool implements Extension {
|
|
|
247
287
|
}),
|
|
248
288
|
{ priority: 300 },
|
|
249
289
|
);
|
|
290
|
+
this.bindCanvasInteractionHandlers();
|
|
250
291
|
|
|
251
|
-
this.subscriptions.on(
|
|
252
|
-
|
|
292
|
+
this.subscriptions.on(
|
|
293
|
+
context.eventBus,
|
|
294
|
+
"tool:activated",
|
|
295
|
+
this.onToolActivated,
|
|
296
|
+
);
|
|
297
|
+
this.subscriptions.on(
|
|
298
|
+
context.eventBus,
|
|
299
|
+
"object:modified",
|
|
300
|
+
this.onObjectModified,
|
|
301
|
+
);
|
|
253
302
|
this.subscriptions.on(
|
|
254
303
|
context.eventBus,
|
|
255
304
|
"selection:created",
|
|
@@ -328,6 +377,8 @@ export class ImageTool implements Extension {
|
|
|
328
377
|
this.imageSpecs = [];
|
|
329
378
|
this.overlaySpecs = [];
|
|
330
379
|
this.imageControlsByCapabilityKey.clear();
|
|
380
|
+
this.clearSnapGuides();
|
|
381
|
+
this.unbindCanvasInteractionHandlers();
|
|
331
382
|
|
|
332
383
|
this.clearRenderedImages();
|
|
333
384
|
this.renderProducerDisposable?.dispose();
|
|
@@ -347,6 +398,7 @@ export class ImageTool implements Extension {
|
|
|
347
398
|
const before = this.isToolActive;
|
|
348
399
|
this.syncToolActiveFromWorkbench(event.id);
|
|
349
400
|
if (!this.isToolActive) {
|
|
401
|
+
this.clearSnapGuides();
|
|
350
402
|
this.setImageFocus(null, {
|
|
351
403
|
syncCanvasSelection: true,
|
|
352
404
|
skipRender: true,
|
|
@@ -396,6 +448,7 @@ export class ImageTool implements Extension {
|
|
|
396
448
|
};
|
|
397
449
|
|
|
398
450
|
private onSelectionCleared = () => {
|
|
451
|
+
this.clearSnapGuides();
|
|
399
452
|
this.setImageFocus(null, {
|
|
400
453
|
syncCanvasSelection: false,
|
|
401
454
|
skipRender: true,
|
|
@@ -405,6 +458,7 @@ export class ImageTool implements Extension {
|
|
|
405
458
|
};
|
|
406
459
|
|
|
407
460
|
private onSceneLayoutChanged = () => {
|
|
461
|
+
this.updateSnapGuideVisuals();
|
|
408
462
|
this.updateImages();
|
|
409
463
|
};
|
|
410
464
|
|
|
@@ -412,6 +466,322 @@ export class ImageTool implements Extension {
|
|
|
412
466
|
this.updateImages();
|
|
413
467
|
};
|
|
414
468
|
|
|
469
|
+
private bindCanvasInteractionHandlers() {
|
|
470
|
+
if (!this.canvasService || this.canvasObjectMovingHandler) return;
|
|
471
|
+
this.canvasObjectMovingHandler = (e: any) => {
|
|
472
|
+
this.handleCanvasObjectMoving(e);
|
|
473
|
+
};
|
|
474
|
+
this.canvasService.canvas.on(
|
|
475
|
+
"object:moving",
|
|
476
|
+
this.canvasObjectMovingHandler,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private unbindCanvasInteractionHandlers() {
|
|
481
|
+
if (!this.canvasService || !this.canvasObjectMovingHandler) return;
|
|
482
|
+
this.canvasService.canvas.off(
|
|
483
|
+
"object:moving",
|
|
484
|
+
this.canvasObjectMovingHandler,
|
|
485
|
+
);
|
|
486
|
+
this.canvasObjectMovingHandler = undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private getActiveImageTarget(target: any): any | null {
|
|
490
|
+
if (!this.isToolActive) return null;
|
|
491
|
+
if (!target) return null;
|
|
492
|
+
if (target?.data?.layerId !== IMAGE_OBJECT_LAYER_ID) return null;
|
|
493
|
+
if (typeof target?.data?.id !== "string") return null;
|
|
494
|
+
return target;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private getTargetBoundsScene(target: any): FrameRect | null {
|
|
498
|
+
if (!this.canvasService || !target) return null;
|
|
499
|
+
const rawBounds =
|
|
500
|
+
typeof target.getBoundingRect === "function"
|
|
501
|
+
? target.getBoundingRect()
|
|
502
|
+
: {
|
|
503
|
+
left: Number(target.left || 0),
|
|
504
|
+
top: Number(target.top || 0),
|
|
505
|
+
width: Number(target.width || 0),
|
|
506
|
+
height: Number(target.height || 0),
|
|
507
|
+
};
|
|
508
|
+
return this.canvasService.toSceneRect({
|
|
509
|
+
left: Number(rawBounds.left || 0),
|
|
510
|
+
top: Number(rawBounds.top || 0),
|
|
511
|
+
width: Number(rawBounds.width || 0),
|
|
512
|
+
height: Number(rawBounds.height || 0),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private getSnapThresholdScene(px: number): number {
|
|
517
|
+
if (!this.canvasService) return px;
|
|
518
|
+
return this.canvasService.toSceneLength(px);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private pickSnapMatch(
|
|
522
|
+
candidates: SnapCandidate[],
|
|
523
|
+
previous: SnapMatch | null,
|
|
524
|
+
): SnapMatch | null {
|
|
525
|
+
if (!candidates.length) return null;
|
|
526
|
+
|
|
527
|
+
const snapThreshold = this.getSnapThresholdScene(
|
|
528
|
+
IMAGE_MOVE_SNAP_THRESHOLD_PX,
|
|
529
|
+
);
|
|
530
|
+
const releaseThreshold = this.getSnapThresholdScene(
|
|
531
|
+
IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
if (previous) {
|
|
535
|
+
const sticky = candidates.find((candidate) => {
|
|
536
|
+
return (
|
|
537
|
+
candidate.lineId === previous.lineId &&
|
|
538
|
+
Math.abs(candidate.deltaScene) <= releaseThreshold
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
if (sticky) return sticky;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
let best: SnapCandidate | null = null;
|
|
545
|
+
candidates.forEach((candidate) => {
|
|
546
|
+
if (Math.abs(candidate.deltaScene) > snapThreshold) return;
|
|
547
|
+
if (!best || Math.abs(candidate.deltaScene) < Math.abs(best.deltaScene)) {
|
|
548
|
+
best = candidate;
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
return best;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private computeMoveSnapMatches(
|
|
555
|
+
target: any,
|
|
556
|
+
frame: FrameRect,
|
|
557
|
+
): { x: SnapMatch | null; y: SnapMatch | null } {
|
|
558
|
+
const bounds = this.getTargetBoundsScene(target);
|
|
559
|
+
if (!bounds || frame.width <= 0 || frame.height <= 0) {
|
|
560
|
+
return { x: null, y: null };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const xCandidates: SnapCandidate[] = [
|
|
564
|
+
{
|
|
565
|
+
axis: "x",
|
|
566
|
+
lineId: "frame-left",
|
|
567
|
+
kind: "edge",
|
|
568
|
+
lineScene: frame.left,
|
|
569
|
+
deltaScene: frame.left - bounds.left,
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
axis: "x",
|
|
573
|
+
lineId: "frame-center-x",
|
|
574
|
+
kind: "center",
|
|
575
|
+
lineScene: frame.left + frame.width / 2,
|
|
576
|
+
deltaScene:
|
|
577
|
+
frame.left + frame.width / 2 - (bounds.left + bounds.width / 2),
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
axis: "x",
|
|
581
|
+
lineId: "frame-right",
|
|
582
|
+
kind: "edge",
|
|
583
|
+
lineScene: frame.left + frame.width,
|
|
584
|
+
deltaScene: frame.left + frame.width - (bounds.left + bounds.width),
|
|
585
|
+
},
|
|
586
|
+
];
|
|
587
|
+
const yCandidates: SnapCandidate[] = [
|
|
588
|
+
{
|
|
589
|
+
axis: "y",
|
|
590
|
+
lineId: "frame-top",
|
|
591
|
+
kind: "edge",
|
|
592
|
+
lineScene: frame.top,
|
|
593
|
+
deltaScene: frame.top - bounds.top,
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
axis: "y",
|
|
597
|
+
lineId: "frame-center-y",
|
|
598
|
+
kind: "center",
|
|
599
|
+
lineScene: frame.top + frame.height / 2,
|
|
600
|
+
deltaScene:
|
|
601
|
+
frame.top + frame.height / 2 - (bounds.top + bounds.height / 2),
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
axis: "y",
|
|
605
|
+
lineId: "frame-bottom",
|
|
606
|
+
kind: "edge",
|
|
607
|
+
lineScene: frame.top + frame.height,
|
|
608
|
+
deltaScene: frame.top + frame.height - (bounds.top + bounds.height),
|
|
609
|
+
},
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
x: this.pickSnapMatch(xCandidates, this.activeSnapX),
|
|
614
|
+
y: this.pickSnapMatch(yCandidates, this.activeSnapY),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private areSnapMatchesEqual(
|
|
619
|
+
a: SnapMatch | null,
|
|
620
|
+
b: SnapMatch | null,
|
|
621
|
+
): boolean {
|
|
622
|
+
if (!a && !b) return true;
|
|
623
|
+
if (!a || !b) return false;
|
|
624
|
+
return a.lineId === b.lineId && a.axis === b.axis && a.kind === b.kind;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private updateSnapMatchState(
|
|
628
|
+
nextX: SnapMatch | null,
|
|
629
|
+
nextY: SnapMatch | null,
|
|
630
|
+
) {
|
|
631
|
+
const changed =
|
|
632
|
+
!this.areSnapMatchesEqual(this.activeSnapX, nextX) ||
|
|
633
|
+
!this.areSnapMatchesEqual(this.activeSnapY, nextY);
|
|
634
|
+
this.activeSnapX = nextX;
|
|
635
|
+
this.activeSnapY = nextY;
|
|
636
|
+
if (changed) {
|
|
637
|
+
this.updateSnapGuideVisuals();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private clearSnapGuides() {
|
|
642
|
+
this.activeSnapX = null;
|
|
643
|
+
this.activeSnapY = null;
|
|
644
|
+
this.removeSnapGuideObject("x");
|
|
645
|
+
this.removeSnapGuideObject("y");
|
|
646
|
+
this.canvasService?.requestRenderAll();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private removeSnapGuideObject(axis: SnapAxis) {
|
|
650
|
+
if (!this.canvasService) return;
|
|
651
|
+
const canvas = this.canvasService.canvas;
|
|
652
|
+
const current =
|
|
653
|
+
axis === "x" ? this.snapGuideXObject : this.snapGuideYObject;
|
|
654
|
+
if (!current) return;
|
|
655
|
+
canvas.remove(current);
|
|
656
|
+
if (axis === "x") {
|
|
657
|
+
this.snapGuideXObject = undefined;
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
this.snapGuideYObject = undefined;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private createOrUpdateSnapGuideObject(axis: SnapAxis, pathData: string) {
|
|
664
|
+
if (!this.canvasService) return;
|
|
665
|
+
const canvas = this.canvasService.canvas;
|
|
666
|
+
const color =
|
|
667
|
+
this.getConfig<string>("image.control.borderColor", "#1677ff") ||
|
|
668
|
+
"#1677ff";
|
|
669
|
+
const strokeWidth = 1;
|
|
670
|
+
this.removeSnapGuideObject(axis);
|
|
671
|
+
|
|
672
|
+
const created = new FabricPath(pathData, {
|
|
673
|
+
originX: "left",
|
|
674
|
+
originY: "top",
|
|
675
|
+
fill: "rgba(0,0,0,0)",
|
|
676
|
+
stroke: color,
|
|
677
|
+
strokeWidth,
|
|
678
|
+
selectable: false,
|
|
679
|
+
evented: false,
|
|
680
|
+
excludeFromExport: true,
|
|
681
|
+
objectCaching: false,
|
|
682
|
+
data: {
|
|
683
|
+
id: `${IMAGE_SNAP_GUIDE_LAYER_ID}.${axis}`,
|
|
684
|
+
layerId: IMAGE_SNAP_GUIDE_LAYER_ID,
|
|
685
|
+
type: "image-snap-guide",
|
|
686
|
+
},
|
|
687
|
+
} as any);
|
|
688
|
+
created.setCoords();
|
|
689
|
+
canvas.add(created);
|
|
690
|
+
canvas.bringObjectToFront(created);
|
|
691
|
+
if (axis === "x") {
|
|
692
|
+
this.snapGuideXObject = created;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
this.snapGuideYObject = created;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private updateSnapGuideVisuals() {
|
|
699
|
+
if (!this.canvasService || !this.isImageEditingVisible()) {
|
|
700
|
+
this.removeSnapGuideObject("x");
|
|
701
|
+
this.removeSnapGuideObject("y");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const frame = this.getFrameRect();
|
|
706
|
+
if (frame.width <= 0 || frame.height <= 0) {
|
|
707
|
+
this.removeSnapGuideObject("x");
|
|
708
|
+
this.removeSnapGuideObject("y");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const frameScreen = this.getFrameRectScreen(frame);
|
|
712
|
+
|
|
713
|
+
if (this.activeSnapX) {
|
|
714
|
+
const x = this.canvasService.toScreenPoint({
|
|
715
|
+
x: this.activeSnapX.lineScene,
|
|
716
|
+
y: frame.top,
|
|
717
|
+
}).x;
|
|
718
|
+
this.createOrUpdateSnapGuideObject(
|
|
719
|
+
"x",
|
|
720
|
+
`M ${x} ${frameScreen.top} L ${x} ${frameScreen.top + frameScreen.height}`,
|
|
721
|
+
);
|
|
722
|
+
} else {
|
|
723
|
+
this.removeSnapGuideObject("x");
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (this.activeSnapY) {
|
|
727
|
+
const y = this.canvasService.toScreenPoint({
|
|
728
|
+
x: frame.left,
|
|
729
|
+
y: this.activeSnapY.lineScene,
|
|
730
|
+
}).y;
|
|
731
|
+
this.createOrUpdateSnapGuideObject(
|
|
732
|
+
"y",
|
|
733
|
+
`M ${frameScreen.left} ${y} L ${frameScreen.left + frameScreen.width} ${y}`,
|
|
734
|
+
);
|
|
735
|
+
} else {
|
|
736
|
+
this.removeSnapGuideObject("y");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
this.canvasService.requestRenderAll();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private handleCanvasObjectMoving(e: any) {
|
|
743
|
+
const target = this.getActiveImageTarget(e?.target);
|
|
744
|
+
if (!target || !this.canvasService) return;
|
|
745
|
+
|
|
746
|
+
const frame = this.getFrameRect();
|
|
747
|
+
if (frame.width <= 0 || frame.height <= 0) {
|
|
748
|
+
this.clearSnapGuides();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const matches = this.computeMoveSnapMatches(target, frame);
|
|
753
|
+
const deltaX = matches.x?.deltaScene ?? 0;
|
|
754
|
+
const deltaY = matches.y?.deltaScene ?? 0;
|
|
755
|
+
|
|
756
|
+
if (deltaX || deltaY) {
|
|
757
|
+
target.set({
|
|
758
|
+
left:
|
|
759
|
+
Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
|
|
760
|
+
top:
|
|
761
|
+
Number(target.top || 0) + this.canvasService.toScreenLength(deltaY),
|
|
762
|
+
});
|
|
763
|
+
target.setCoords();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
this.updateSnapMatchState(matches.x, matches.y);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private applySnapMatchesToTarget(
|
|
770
|
+
target: any,
|
|
771
|
+
matches: { x: SnapMatch | null; y: SnapMatch | null },
|
|
772
|
+
) {
|
|
773
|
+
if (!this.canvasService || !target) return;
|
|
774
|
+
const deltaX = matches.x?.deltaScene ?? 0;
|
|
775
|
+
const deltaY = matches.y?.deltaScene ?? 0;
|
|
776
|
+
if (!deltaX && !deltaY) return;
|
|
777
|
+
|
|
778
|
+
target.set({
|
|
779
|
+
left: Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
|
|
780
|
+
top: Number(target.top || 0) + this.canvasService.toScreenLength(deltaY),
|
|
781
|
+
});
|
|
782
|
+
target.setCoords();
|
|
783
|
+
}
|
|
784
|
+
|
|
415
785
|
private syncToolActiveFromWorkbench(fallbackId?: string | null) {
|
|
416
786
|
const wb = this.context?.services.get<WorkbenchService>("WorkbenchService");
|
|
417
787
|
const activeId = wb?.activeToolId;
|
|
@@ -1589,6 +1959,7 @@ export class ImageTool implements Extension {
|
|
|
1589
1959
|
isImageSelectionActive: this.isImageSelectionActive,
|
|
1590
1960
|
focusedImageId: this.focusedImageId,
|
|
1591
1961
|
});
|
|
1962
|
+
this.updateSnapGuideVisuals();
|
|
1592
1963
|
this.canvasService.requestRenderAll();
|
|
1593
1964
|
}
|
|
1594
1965
|
|
|
@@ -1605,6 +1976,9 @@ export class ImageTool implements Extension {
|
|
|
1605
1976
|
|
|
1606
1977
|
const frame = this.getFrameRect();
|
|
1607
1978
|
if (!frame.width || !frame.height) return;
|
|
1979
|
+
const matches = this.computeMoveSnapMatches(target, frame);
|
|
1980
|
+
this.applySnapMatchesToTarget(target, matches);
|
|
1981
|
+
this.clearSnapGuides();
|
|
1608
1982
|
|
|
1609
1983
|
const center = target.getCenterPoint
|
|
1610
1984
|
? target.getCenterPoint()
|