@pixerate/editor 0.39.0 → 0.40.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/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/vector/index.cjs +414 -155
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +83 -9
- package/dist/vector/index.d.ts +83 -9
- package/dist/vector/index.js +295 -36
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
package/dist/vector/index.js
CHANGED
|
@@ -829,7 +829,11 @@ var capExtent = (style, width) => style.strokeLinecap === "round" || style.strok
|
|
|
829
829
|
function arrowheadWeight(style, end) {
|
|
830
830
|
const scale = end === "start" ? style.startArrowScale : style.endArrowScale;
|
|
831
831
|
const s = typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? scale : 1;
|
|
832
|
-
|
|
832
|
+
const width = style.strokeWidth ?? 1;
|
|
833
|
+
const profile = strokeWidthProfileOf(style);
|
|
834
|
+
const at2 = profile ? widthProfileAt(profile, end === "start" ? 0 : 1) : null;
|
|
835
|
+
const here = at2 ? Math.max(width, width * (at2.left + at2.right) / 2) : width;
|
|
836
|
+
return Math.max(0.25, here * s);
|
|
833
837
|
}
|
|
834
838
|
function arrowheadInset(kind, hw, cap) {
|
|
835
839
|
const l = 4 * hw;
|
|
@@ -4078,6 +4082,12 @@ function vectorStrokeWidthGeometry(node) {
|
|
|
4078
4082
|
const parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);
|
|
4079
4083
|
return parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;
|
|
4080
4084
|
}
|
|
4085
|
+
function shiftWidthPoints(profile, indices, dt) {
|
|
4086
|
+
const moving = new Set(indices);
|
|
4087
|
+
const tagged = profile.map((p, i) => ({ p: moving.has(i) ? { ...p, t: Math.min(Math.max(p.t + dt, 0), 1) } : { ...p }, i }));
|
|
4088
|
+
tagged.sort((a, b) => a.p.t - b.p.t || a.i - b.i);
|
|
4089
|
+
return { points: tagged.map((x) => x.p), renumber: new Map(tagged.map((x, k) => [x.i, k])) };
|
|
4090
|
+
}
|
|
4081
4091
|
function getVectorWidthHandles(state) {
|
|
4082
4092
|
if (state.tool !== "width") return [];
|
|
4083
4093
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -4087,7 +4097,7 @@ function getVectorWidthHandles(state) {
|
|
|
4087
4097
|
}
|
|
4088
4098
|
if (state.hover?.nodeId) ids.add(state.hover.nodeId);
|
|
4089
4099
|
if (state.interaction?.type === "width") ids.add(state.interaction.ref.nodeId);
|
|
4090
|
-
|
|
4100
|
+
for (const r of state.selectedWidthPoints) ids.add(r.nodeId);
|
|
4091
4101
|
const out = [];
|
|
4092
4102
|
for (const id of ids) {
|
|
4093
4103
|
const node = findVectorNode(state.document.nodes, id);
|
|
@@ -4106,7 +4116,6 @@ function getVectorWidthHandles(state) {
|
|
|
4106
4116
|
const p = applyMatrix(m, c);
|
|
4107
4117
|
const l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });
|
|
4108
4118
|
const r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });
|
|
4109
|
-
const sel = state.selectedWidthPoint;
|
|
4110
4119
|
out.push({
|
|
4111
4120
|
ref: { nodeId: id, index, subpath: sp },
|
|
4112
4121
|
x: p.x,
|
|
@@ -4116,7 +4125,7 @@ function getVectorWidthHandles(state) {
|
|
|
4116
4125
|
t: w.t,
|
|
4117
4126
|
leftScale: w.left,
|
|
4118
4127
|
rightScale: w.right,
|
|
4119
|
-
selected:
|
|
4128
|
+
selected: state.selectedWidthPoints.some((r2) => r2.nodeId === id && r2.index === index)
|
|
4120
4129
|
});
|
|
4121
4130
|
});
|
|
4122
4131
|
}
|
|
@@ -4312,7 +4321,8 @@ var VectorEditorController = class {
|
|
|
4312
4321
|
doc;
|
|
4313
4322
|
selection = [];
|
|
4314
4323
|
anchors = [];
|
|
4315
|
-
|
|
4324
|
+
widthPoints = [];
|
|
4325
|
+
widthEdit = null;
|
|
4316
4326
|
editingId = null;
|
|
4317
4327
|
pen = null;
|
|
4318
4328
|
tool;
|
|
@@ -4398,7 +4408,8 @@ var VectorEditorController = class {
|
|
|
4398
4408
|
tool: this.tool,
|
|
4399
4409
|
selection: this.selection,
|
|
4400
4410
|
selectedAnchors: this.anchors,
|
|
4401
|
-
|
|
4411
|
+
selectedWidthPoints: this.widthPoints,
|
|
4412
|
+
widthPointEdit: this.widthEdit,
|
|
4402
4413
|
editingId: this.editingId,
|
|
4403
4414
|
selectionBounds: this.boundsOf(this.selection),
|
|
4404
4415
|
selectionFrame: this.frameOf(this.selectedRoots()),
|
|
@@ -4469,7 +4480,7 @@ var VectorEditorController = class {
|
|
|
4469
4480
|
case "pen":
|
|
4470
4481
|
return h?.anchor ? "pointer" : "crosshair";
|
|
4471
4482
|
case "width":
|
|
4472
|
-
if (this.drag?.kind === "width") return this.drag.mode === "
|
|
4483
|
+
if (this.drag?.kind === "width") return this.drag.mode === "width" ? "ew-resize" : "grabbing";
|
|
4473
4484
|
if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
|
|
4474
4485
|
return h?.strokeSpot ? "copy" : "default";
|
|
4475
4486
|
case "shapeBuilder":
|
|
@@ -4489,6 +4500,7 @@ var VectorEditorController = class {
|
|
|
4489
4500
|
}
|
|
4490
4501
|
restore(s) {
|
|
4491
4502
|
const docChanged = s.doc !== this.doc || this.committing;
|
|
4503
|
+
if (!this.committing) this.widthEdit = null;
|
|
4492
4504
|
this.committing = false;
|
|
4493
4505
|
this.applying = true;
|
|
4494
4506
|
try {
|
|
@@ -4497,6 +4509,13 @@ var VectorEditorController = class {
|
|
|
4497
4509
|
this.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));
|
|
4498
4510
|
this.editingId = s.editingId && this.getNode(s.editingId) ? s.editingId : null;
|
|
4499
4511
|
this.pen = s.pen && this.getNode(s.pen.nodeId)?.subpaths?.[s.pen.subpath] ? s.pen : null;
|
|
4512
|
+
if (this.widthPoints.length) {
|
|
4513
|
+
const kept = this.widthPoints.filter((r) => {
|
|
4514
|
+
const n = this.getNode(r.nodeId);
|
|
4515
|
+
return n?.type === "path" && !!n.strokeWidthProfile?.[r.index];
|
|
4516
|
+
});
|
|
4517
|
+
if (kept.length !== this.widthPoints.length) this.widthPoints = kept;
|
|
4518
|
+
}
|
|
4500
4519
|
} finally {
|
|
4501
4520
|
this.applying = false;
|
|
4502
4521
|
}
|
|
@@ -4602,7 +4621,8 @@ var VectorEditorController = class {
|
|
|
4602
4621
|
this.tool = tool;
|
|
4603
4622
|
this.hover = null;
|
|
4604
4623
|
this.guides = null;
|
|
4605
|
-
this.
|
|
4624
|
+
this.widthPoints = [];
|
|
4625
|
+
this.widthEdit = null;
|
|
4606
4626
|
this.builderPreview = null;
|
|
4607
4627
|
this.builderHoverKey = null;
|
|
4608
4628
|
if (tool !== "direct") {
|
|
@@ -4878,18 +4898,143 @@ var VectorEditorController = class {
|
|
|
4878
4898
|
const p = profile;
|
|
4879
4899
|
points = p.taper || p.wave ? generateWidthProfile(p) : [];
|
|
4880
4900
|
}
|
|
4881
|
-
this.
|
|
4901
|
+
this.widthPoints = [];
|
|
4902
|
+
this.widthEdit = null;
|
|
4882
4903
|
return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
|
|
4883
4904
|
}
|
|
4884
|
-
/** Removes one width point of a path's profile
|
|
4905
|
+
/** Removes one width point of a path's profile. */
|
|
4885
4906
|
removeWidthPoint(ref) {
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4907
|
+
return this.removeWidthPoints([ref]);
|
|
4908
|
+
}
|
|
4909
|
+
/** Removes width points (default: the selected ones — the width tool's Delete), as one undo step. */
|
|
4910
|
+
removeWidthPoints(refs = this.widthPoints) {
|
|
4911
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
4912
|
+
for (const r of refs) {
|
|
4913
|
+
const node = this.getNode(r.nodeId);
|
|
4914
|
+
if (node?.type !== "path" || !node.strokeWidthProfile?.[r.index]) continue;
|
|
4915
|
+
byNode.set(r.nodeId, [...byNode.get(r.nodeId) ?? [], r.index]);
|
|
4916
|
+
}
|
|
4917
|
+
if (!byNode.size) return { ok: false, message: "There is no such width point." };
|
|
4918
|
+
const count = [...byNode.values()].reduce((n, l) => n + new Set(l).size, 0);
|
|
4919
|
+
this.widthPoints = [];
|
|
4920
|
+
this.widthEdit = null;
|
|
4921
|
+
this.change(count > 1 ? "Delete Width Points" : "Delete Width Point", () => {
|
|
4922
|
+
for (const [id, indices] of byNode) {
|
|
4923
|
+
const node = this.getNode(id);
|
|
4924
|
+
const rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), indices);
|
|
4925
|
+
this.replaceNode(id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null }));
|
|
4926
|
+
}
|
|
4927
|
+
});
|
|
4928
|
+
return { ok: true };
|
|
4929
|
+
}
|
|
4930
|
+
/** Selects width points (width tool); points on other paths than the first are dropped. */
|
|
4931
|
+
selectWidthPoints(refs) {
|
|
4932
|
+
const first = refs[0]?.nodeId;
|
|
4933
|
+
const next = [];
|
|
4934
|
+
for (const r of refs) {
|
|
4935
|
+
if (r.nodeId !== first || next.some((q) => q.index === r.index)) continue;
|
|
4936
|
+
const n = this.getNode(r.nodeId);
|
|
4937
|
+
if (n?.type === "path" && n.strokeWidthProfile?.[r.index]) next.push({ nodeId: r.nodeId, index: r.index, subpath: r.subpath ?? 0 });
|
|
4938
|
+
}
|
|
4939
|
+
this.widthPoints = next;
|
|
4940
|
+
this.notify();
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Sets one width point numerically: `index` names an existing point of path `nodeId`, null
|
|
4944
|
+
* (or left out) adds one at `values.t`. `left` / `right` are side widths in document units
|
|
4945
|
+
* (how far the stroke reaches on each side of the path), `t` the position along each subpath
|
|
4946
|
+
* (0–1). A point moved onto another one's `t` makes a step there. Selects the point and
|
|
4947
|
+
* refreshes an open width-point editor; one undo step. Ported from ArtCraft vectorcraft's
|
|
4948
|
+
* Width Point Edit dialog (`crates/tools/src/distort/width.rs`, MIT OR Apache-2.0).
|
|
4949
|
+
*/
|
|
4950
|
+
setWidthPoint(target, values) {
|
|
4951
|
+
const node = this.getNode(target.nodeId);
|
|
4952
|
+
if (node?.type !== "path") return { ok: false, message: "There is no such path." };
|
|
4953
|
+
const g = vectorStrokeWidthGeometry(node);
|
|
4954
|
+
if (!g || !(g.unit > 0)) return { ok: false, message: "The path has no visible stroke." };
|
|
4955
|
+
const num = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
|
|
4956
|
+
const t = num(values.t);
|
|
4957
|
+
const left = num(values.left);
|
|
4958
|
+
const right = num(values.right);
|
|
4959
|
+
const scale = (w) => w === void 0 ? void 0 : Math.max(0, w) / g.unit;
|
|
4960
|
+
const profile = sanitizeWidthProfile(node.strokeWidthProfile);
|
|
4961
|
+
let points;
|
|
4962
|
+
let index;
|
|
4963
|
+
const existing = target.index ?? null;
|
|
4964
|
+
if (existing === null) {
|
|
4965
|
+
if (t === void 0) return { ok: false, message: "A new width point needs a position t." };
|
|
4966
|
+
if (profile.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { ok: false, message: "The profile has too many width points." };
|
|
4967
|
+
({ points, index } = insertWidthPoint(profile.length ? profile : null, t, { left: scale(left), right: scale(right) }, { step: true }));
|
|
4968
|
+
} else {
|
|
4969
|
+
const p = profile[existing];
|
|
4970
|
+
if (!p) return { ok: false, message: "There is no such width point." };
|
|
4971
|
+
const sides2 = { left: scale(left) ?? p.left, right: scale(right) ?? p.right };
|
|
4972
|
+
const tt = t === void 0 ? p.t : Math.min(Math.max(t, 0), 1);
|
|
4973
|
+
if (Math.abs(tt - p.t) <= VECTOR_WIDTH_SAME_T) {
|
|
4974
|
+
points = setWidthPoint(profile, existing, sides2);
|
|
4975
|
+
index = existing;
|
|
4976
|
+
} else {
|
|
4977
|
+
const rest = removeWidthPoints(profile, [existing]);
|
|
4978
|
+
if (rest.length) ({ points, index } = insertWidthPoint(rest, tt, sides2, { step: true }));
|
|
4979
|
+
else {
|
|
4980
|
+
points = [{ t: tt, ...sides2 }];
|
|
4981
|
+
index = 0;
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
const subpath = this.widthEdit?.nodeId === node.id ? this.widthEdit.subpath : this.widthPoints.find((r) => r.nodeId === node.id)?.subpath ?? 0;
|
|
4986
|
+
const edit = this.widthEdit;
|
|
4987
|
+
this.change("Width Point", () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: points })));
|
|
4988
|
+
this.widthPoints = [{ nodeId: node.id, index, subpath }];
|
|
4989
|
+
if (edit?.nodeId === node.id && (edit.index === existing || existing === null)) this.widthEdit = this.widthEditFor(node.id, subpath, index, null);
|
|
4990
|
+
this.notify();
|
|
4891
4991
|
return { ok: true };
|
|
4892
4992
|
}
|
|
4993
|
+
/**
|
|
4994
|
+
* Opens the width-point editor (what a double-click with the width tool does) on a width
|
|
4995
|
+
* point, or on a spot of a stroke (`t` without `index`: applying it adds a point there).
|
|
4996
|
+
* Returns false when the path has no visible stroke.
|
|
4997
|
+
*/
|
|
4998
|
+
openWidthPointEdit(target) {
|
|
4999
|
+
const edit = this.widthEditFor(target.nodeId, target.subpath ?? 0, target.index ?? null, target.t ?? null);
|
|
5000
|
+
if (!edit) return false;
|
|
5001
|
+
this.widthEdit = edit;
|
|
5002
|
+
this.widthPoints = edit.index === null ? [] : [{ nodeId: edit.nodeId, index: edit.index, subpath: edit.subpath }];
|
|
5003
|
+
this.notify();
|
|
5004
|
+
return true;
|
|
5005
|
+
}
|
|
5006
|
+
/** Closes the width-point editor without changing anything. */
|
|
5007
|
+
closeWidthPointEdit() {
|
|
5008
|
+
if (!this.widthEdit) return;
|
|
5009
|
+
this.widthEdit = null;
|
|
5010
|
+
this.notify();
|
|
5011
|
+
}
|
|
5012
|
+
/** Applies `values` to the width point being edited (see `setWidthPoint`) and closes the editor. */
|
|
5013
|
+
commitWidthPointEdit(values) {
|
|
5014
|
+
const edit = this.widthEdit;
|
|
5015
|
+
if (!edit) return { ok: false, message: "No width point is being edited." };
|
|
5016
|
+
const r = this.setWidthPoint({ nodeId: edit.nodeId, index: edit.index }, edit.index === null ? { t: edit.t, ...values } : values);
|
|
5017
|
+
if (r.ok) this.closeWidthPointEdit();
|
|
5018
|
+
return r;
|
|
5019
|
+
}
|
|
5020
|
+
/** The editor's view of point `index` (or of the stroke at `t` when index is null) of `nodeId`. */
|
|
5021
|
+
widthEditFor(nodeId, subpath, index, t) {
|
|
5022
|
+
const node = this.getNode(nodeId);
|
|
5023
|
+
if (node?.type !== "path") return null;
|
|
5024
|
+
const g = vectorStrokeWidthGeometry(node);
|
|
5025
|
+
if (!g) return null;
|
|
5026
|
+
const profile = sanitizeWidthProfile(node.strokeWidthProfile);
|
|
5027
|
+
const p = index === null ? null : profile[index];
|
|
5028
|
+
if (index !== null && !p) return null;
|
|
5029
|
+
const tt = p ? p.t : Math.min(Math.max(t ?? 0, 0), 1);
|
|
5030
|
+
const sides2 = p ?? widthProfileAt(profile.length ? profile : null, tt);
|
|
5031
|
+
const sp = Math.min(Math.max(0, subpath), Math.max(0, node.subpaths.length - 1));
|
|
5032
|
+
const at2 = strokePositionPoint(g.line, sp, tt);
|
|
5033
|
+
const round = (v2) => Math.round(v2 * 1e6) / 1e6;
|
|
5034
|
+
const q = at2 ? applyMatrix(this.worldOf(nodeId), { x: at2.point[0], y: at2.point[1] }) : { x: 0, y: 0 };
|
|
5035
|
+
const point = { x: round(q.x), y: round(q.y) };
|
|
5036
|
+
return { nodeId, subpath: sp, index, t: round(tt), left: round(sides2.left * g.unit), right: round(sides2.right * g.unit), point };
|
|
5037
|
+
}
|
|
4893
5038
|
/**
|
|
4894
5039
|
* Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
|
|
4895
5040
|
* anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
|
|
@@ -6298,13 +6443,19 @@ var VectorEditorController = class {
|
|
|
6298
6443
|
}
|
|
6299
6444
|
}
|
|
6300
6445
|
widthView() {
|
|
6301
|
-
return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction,
|
|
6446
|
+
return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoints: this.widthPoints };
|
|
6302
6447
|
}
|
|
6303
6448
|
/** The width point centre or side end under `screen` (width tool). */
|
|
6304
6449
|
hitWidthHandle(screen) {
|
|
6450
|
+
const shown = this.hitWidthHandleIn(screen, this.widthView());
|
|
6451
|
+
if (shown) return shown;
|
|
6452
|
+
const spot = this.strokeSpotAt(this.screenToDocument(screen));
|
|
6453
|
+
return spot && spot.nodeId !== this.hover?.nodeId ? this.hitWidthHandleIn(screen, { ...this.widthView(), hover: { nodeId: spot.nodeId } }) : null;
|
|
6454
|
+
}
|
|
6455
|
+
hitWidthHandleIn(screen, view) {
|
|
6305
6456
|
let best = null;
|
|
6306
6457
|
let bestD = Infinity;
|
|
6307
|
-
for (const h of getVectorWidthHandles(
|
|
6458
|
+
for (const h of getVectorWidthHandles(view)) {
|
|
6308
6459
|
for (const [part, q] of [["center", h], ["left", h.left], ["right", h.right]]) {
|
|
6309
6460
|
const s = this.documentToScreen(q);
|
|
6310
6461
|
const d = Math.hypot(s.x - screen.x, s.y - screen.y);
|
|
@@ -6357,11 +6508,10 @@ var VectorEditorController = class {
|
|
|
6357
6508
|
const nodeId = hit?.ref.nodeId ?? spot?.nodeId;
|
|
6358
6509
|
const node = nodeId ? this.getNode(nodeId) : void 0;
|
|
6359
6510
|
const g = node?.type === "path" ? vectorStrokeWidthGeometry(node) : null;
|
|
6511
|
+
if (this.widthEdit) this.widthEdit = null;
|
|
6360
6512
|
if (!node || node.type !== "path" || !g) {
|
|
6361
|
-
if (
|
|
6362
|
-
|
|
6363
|
-
this.notify();
|
|
6364
|
-
}
|
|
6513
|
+
if (!input.shift) this.widthPoints = [];
|
|
6514
|
+
this.notify();
|
|
6365
6515
|
return;
|
|
6366
6516
|
}
|
|
6367
6517
|
const before = this.snapshot();
|
|
@@ -6370,6 +6520,8 @@ var VectorEditorController = class {
|
|
|
6370
6520
|
let ref;
|
|
6371
6521
|
let mode = "width";
|
|
6372
6522
|
let side = "both";
|
|
6523
|
+
let group = [];
|
|
6524
|
+
let shift = false;
|
|
6373
6525
|
let t;
|
|
6374
6526
|
let subpath;
|
|
6375
6527
|
if (hit) {
|
|
@@ -6377,8 +6529,20 @@ var VectorEditorController = class {
|
|
|
6377
6529
|
ref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };
|
|
6378
6530
|
t = profile[ref.index]?.t ?? 0;
|
|
6379
6531
|
subpath = hit.ref.subpath;
|
|
6380
|
-
if (hit.ref.part === "center") mode = "move";
|
|
6532
|
+
if (hit.ref.part === "center") mode = input.alt && profile.length < VECTOR_WIDTH_PROFILE_MAX_POINTS ? "copy" : "move";
|
|
6381
6533
|
else if (input.alt) side = hit.ref.part;
|
|
6534
|
+
const same = this.widthPoints.length > 0 && this.widthPoints[0].nodeId === node.id;
|
|
6535
|
+
const selected = same && this.widthPoints.some((r) => r.index === ref.index);
|
|
6536
|
+
if (input.shift && mode !== "copy") {
|
|
6537
|
+
if (selected) {
|
|
6538
|
+
this.widthPoints = this.widthPoints.filter((r) => r.index !== ref.index);
|
|
6539
|
+
this.notify();
|
|
6540
|
+
return;
|
|
6541
|
+
}
|
|
6542
|
+
this.widthPoints = same ? [...this.widthPoints, ref] : [ref];
|
|
6543
|
+
shift = true;
|
|
6544
|
+
} else if (mode === "copy" || !selected) this.widthPoints = [ref];
|
|
6545
|
+
if (mode !== "copy") group = this.widthPoints.filter((r) => r.index !== ref.index).map((r) => r.index);
|
|
6382
6546
|
} else {
|
|
6383
6547
|
const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
|
|
6384
6548
|
const inserted = insertWidthPoint(strokeWidthProfileOf(node), near2.t);
|
|
@@ -6387,17 +6551,21 @@ var VectorEditorController = class {
|
|
|
6387
6551
|
t = near2.t;
|
|
6388
6552
|
ref = { nodeId: node.id, index: inserted.index, subpath };
|
|
6389
6553
|
if (input.alt) side = near2.side;
|
|
6554
|
+
this.widthPoints = [ref];
|
|
6390
6555
|
}
|
|
6391
6556
|
const at2 = strokePositionPoint(g.line, subpath, t);
|
|
6392
6557
|
if (!at2) return;
|
|
6393
6558
|
const base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
|
|
6394
6559
|
this.doc = base;
|
|
6395
|
-
this.widthPoint = { nodeId: node.id, index: ref.index, subpath };
|
|
6396
6560
|
this.drag = {
|
|
6397
6561
|
kind: "width",
|
|
6398
6562
|
ref,
|
|
6399
6563
|
mode,
|
|
6400
6564
|
side,
|
|
6565
|
+
source: ref.index,
|
|
6566
|
+
group,
|
|
6567
|
+
shift,
|
|
6568
|
+
renumber: null,
|
|
6401
6569
|
origin: { x: at2.point[0], y: at2.point[1] },
|
|
6402
6570
|
normal: { x: at2.tangent[1], y: -at2.tangent[0] },
|
|
6403
6571
|
unit: g.unit,
|
|
@@ -6412,6 +6580,31 @@ var VectorEditorController = class {
|
|
|
6412
6580
|
this.interaction = { type: "width", ref, mode, width: (w.left + w.right) * g.unit };
|
|
6413
6581
|
this.notify(!hit);
|
|
6414
6582
|
}
|
|
6583
|
+
/**
|
|
6584
|
+
* A width-tool slide to `t` on the dragged point's subpath lands exactly on another width
|
|
6585
|
+
* point (not `skip`) within the pick distance on screen: dropping a point onto another makes
|
|
6586
|
+
* a discontinuous point (two points at one `t`, the width steps there).
|
|
6587
|
+
*/
|
|
6588
|
+
snapWidthT(drag, node, line2, t, skip) {
|
|
6589
|
+
const m = this.worldOf(node.id, drag.base);
|
|
6590
|
+
const at2 = strokePositionPoint(line2, drag.ref.subpath, t);
|
|
6591
|
+
if (!at2) return t;
|
|
6592
|
+
const here = this.documentToScreen(applyMatrix(m, { x: at2.point[0], y: at2.point[1] }));
|
|
6593
|
+
let best = t;
|
|
6594
|
+
let bestD = this.hitRadius;
|
|
6595
|
+
drag.profile.forEach((w, i) => {
|
|
6596
|
+
if (i === skip || drag.group.includes(i)) return;
|
|
6597
|
+
const q = strokePositionPoint(line2, drag.ref.subpath, w.t);
|
|
6598
|
+
if (!q) return;
|
|
6599
|
+
const s = this.documentToScreen(applyMatrix(m, { x: q.point[0], y: q.point[1] }));
|
|
6600
|
+
const d = Math.hypot(s.x - here.x, s.y - here.y);
|
|
6601
|
+
if (d <= bestD) {
|
|
6602
|
+
best = w.t;
|
|
6603
|
+
bestD = d;
|
|
6604
|
+
}
|
|
6605
|
+
});
|
|
6606
|
+
return best;
|
|
6607
|
+
}
|
|
6415
6608
|
/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
|
|
6416
6609
|
nearestOutline(p) {
|
|
6417
6610
|
let best = null;
|
|
@@ -6896,19 +7089,35 @@ var VectorEditorController = class {
|
|
|
6896
7089
|
drag.moved = true;
|
|
6897
7090
|
const node = findVectorNode(drag.base.nodes, drag.ref.nodeId);
|
|
6898
7091
|
const local = this.toLocal(node.id, p, drag.base);
|
|
7092
|
+
const src = drag.profile[drag.source];
|
|
6899
7093
|
let profile;
|
|
6900
|
-
|
|
7094
|
+
let index = drag.source;
|
|
7095
|
+
if (drag.mode !== "width") {
|
|
6901
7096
|
const g = vectorStrokeWidthGeometry(node);
|
|
6902
7097
|
const near2 = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;
|
|
6903
|
-
if (!near2) return;
|
|
6904
|
-
|
|
7098
|
+
if (!g || !near2) return;
|
|
7099
|
+
const t = drag.group.length ? near2.t : this.snapWidthT(drag, node, g.line, near2.t, drag.source);
|
|
7100
|
+
if (drag.mode === "copy") {
|
|
7101
|
+
({ points: profile, index } = insertWidthPoint(drag.profile, t, { left: src.left, right: src.right }, { step: true }));
|
|
7102
|
+
} else if (drag.group.length) {
|
|
7103
|
+
const shifted = shiftWidthPoints(drag.profile, [drag.source, ...drag.group], t - src.t);
|
|
7104
|
+
profile = shifted.points;
|
|
7105
|
+
drag.renumber = shifted.renumber;
|
|
7106
|
+
index = shifted.renumber.get(drag.source) ?? drag.source;
|
|
7107
|
+
} else profile = setWidthPoint(drag.profile, drag.source, { t });
|
|
6905
7108
|
} else {
|
|
6906
7109
|
const off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;
|
|
6907
7110
|
const patch = drag.side === "both" ? { left: Math.abs(off), right: Math.abs(off) } : drag.side === "left" ? { left: Math.max(0, off) } : { right: Math.max(0, -off) };
|
|
6908
|
-
profile = setWidthPoint(drag.profile, drag.
|
|
7111
|
+
profile = setWidthPoint(drag.profile, drag.source, patch);
|
|
7112
|
+
if (drag.group.length) {
|
|
7113
|
+
const dl = profile[drag.source].left - src.left;
|
|
7114
|
+
const dr = profile[drag.source].right - src.right;
|
|
7115
|
+
for (const i of drag.group) profile = setWidthPoint(profile, i, { left: Math.max(0, drag.profile[i].left + dl), right: Math.max(0, drag.profile[i].right + dr) });
|
|
7116
|
+
}
|
|
6909
7117
|
}
|
|
6910
7118
|
this.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
|
|
6911
|
-
|
|
7119
|
+
drag.ref = { ...drag.ref, index };
|
|
7120
|
+
const w = profile[index];
|
|
6912
7121
|
this.interaction = { type: "width", ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };
|
|
6913
7122
|
this.notify(true);
|
|
6914
7123
|
return;
|
|
@@ -7054,12 +7263,23 @@ var VectorEditorController = class {
|
|
|
7054
7263
|
return;
|
|
7055
7264
|
}
|
|
7056
7265
|
case "width": {
|
|
7057
|
-
if (drag.moved)
|
|
7266
|
+
if (drag.moved) {
|
|
7267
|
+
const several = drag.group.length > 0;
|
|
7268
|
+
if (drag.mode === "copy") this.widthPoints = [drag.ref];
|
|
7269
|
+
else if (drag.mode === "move" && several) {
|
|
7270
|
+
this.widthPoints = [drag.source, ...drag.group].map((i) => ({ ...drag.ref, index: drag.renumber?.get(i) ?? i }));
|
|
7271
|
+
} else if (!several) this.widthPoints = [drag.ref];
|
|
7272
|
+
const label = drag.mode === "copy" ? "Copy Width Point" : drag.mode === "move" ? several ? "Move Width Points" : "Move Width Point" : several ? "Width Points" : "Width Point";
|
|
7273
|
+
return this.commit(label, drag.before);
|
|
7274
|
+
}
|
|
7058
7275
|
if (drag.created) {
|
|
7059
7276
|
this.doc = drag.before.doc;
|
|
7060
|
-
this.
|
|
7277
|
+
this.widthPoints = [];
|
|
7061
7278
|
this.notify(true);
|
|
7062
|
-
} else
|
|
7279
|
+
} else {
|
|
7280
|
+
if (!drag.shift && drag.group.length) this.widthPoints = [drag.ref];
|
|
7281
|
+
this.notify();
|
|
7282
|
+
}
|
|
7063
7283
|
return;
|
|
7064
7284
|
}
|
|
7065
7285
|
case "builder":
|
|
@@ -7148,7 +7368,10 @@ var VectorEditorController = class {
|
|
|
7148
7368
|
}
|
|
7149
7369
|
this.restore(drag.before);
|
|
7150
7370
|
}
|
|
7151
|
-
/**
|
|
7371
|
+
/**
|
|
7372
|
+
* Double click: select tool enters path editing, direct tool toggles an anchor or adds one,
|
|
7373
|
+
* width tool opens the width-point editor (`widthPointEdit`).
|
|
7374
|
+
*/
|
|
7152
7375
|
doubleClick(screen) {
|
|
7153
7376
|
const p = this.screenToDocument(screen);
|
|
7154
7377
|
if (this.tool === "select") {
|
|
@@ -7176,6 +7399,16 @@ var VectorEditorController = class {
|
|
|
7176
7399
|
}
|
|
7177
7400
|
return;
|
|
7178
7401
|
}
|
|
7402
|
+
if (this.tool === "width") {
|
|
7403
|
+
const hit = this.hitWidthHandle(screen);
|
|
7404
|
+
if (hit) {
|
|
7405
|
+
this.openWidthPointEdit(hit.ref);
|
|
7406
|
+
return;
|
|
7407
|
+
}
|
|
7408
|
+
const spot = this.strokeSpotAt(p);
|
|
7409
|
+
if (spot) this.openWidthPointEdit({ nodeId: spot.nodeId, subpath: spot.subpath, t: spot.t });
|
|
7410
|
+
return;
|
|
7411
|
+
}
|
|
7179
7412
|
if (this.tool === "pen") this.finishPath();
|
|
7180
7413
|
}
|
|
7181
7414
|
// -------------------------------------------------------------------------
|
|
@@ -7236,8 +7469,9 @@ var VectorEditorController = class {
|
|
|
7236
7469
|
switch (key) {
|
|
7237
7470
|
case "Escape":
|
|
7238
7471
|
if (this.drag) this.cancelDrag();
|
|
7239
|
-
else if (this.
|
|
7240
|
-
|
|
7472
|
+
else if (this.widthEdit) this.closeWidthPointEdit();
|
|
7473
|
+
else if (this.tool === "width" && this.widthPoints.length) {
|
|
7474
|
+
this.widthPoints = [];
|
|
7241
7475
|
this.notify();
|
|
7242
7476
|
} else if (this.pen) this.finishPath();
|
|
7243
7477
|
else if (this.tool === "direct" && this.anchors.length) {
|
|
@@ -7260,7 +7494,7 @@ var VectorEditorController = class {
|
|
|
7260
7494
|
return false;
|
|
7261
7495
|
case "Delete":
|
|
7262
7496
|
case "Backspace":
|
|
7263
|
-
if (this.tool === "width" && this.
|
|
7497
|
+
if (this.tool === "width" && this.widthPoints.length) return this.removeWidthPoints().ok;
|
|
7264
7498
|
if (this.pen) {
|
|
7265
7499
|
const node = this.getNode(this.pen.nodeId);
|
|
7266
7500
|
const sp = node.subpaths[this.pen.subpath];
|
|
@@ -7545,7 +7779,7 @@ function getVectorOverlayRenderData(state) {
|
|
|
7545
7779
|
});
|
|
7546
7780
|
const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
|
|
7547
7781
|
const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
|
|
7548
|
-
if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
|
|
7782
|
+
if (it?.type === "width") status = it.mode === "move" ? "Move width point" : it.mode === "copy" ? "Copy width point" : `W ${fmt(it.width)}`;
|
|
7549
7783
|
let builder = null;
|
|
7550
7784
|
if (state.builder) {
|
|
7551
7785
|
const v2 = state.viewport;
|
|
@@ -7964,6 +8198,31 @@ function createVectorCommands(controller, options = {}) {
|
|
|
7964
8198
|
return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
|
|
7965
8199
|
}
|
|
7966
8200
|
},
|
|
8201
|
+
{
|
|
8202
|
+
...base,
|
|
8203
|
+
id: `${prefix}setWidthPoint`,
|
|
8204
|
+
label: "Width Point\u2026",
|
|
8205
|
+
keywords: ["width point", "variable width", "stroke width at", "taper", "profile"],
|
|
8206
|
+
description: "Sets one width point of a path's stroke profile numerically: `index` names an existing point (see `vector.setWidthProfile`'s result), leave it out to add one at `t`. `left` / `right` are side widths in document units (how far the stroke reaches on each side), `t` the position along each subpath (0\u20131); a point moved onto another's `t` makes a step. Returns the profile.",
|
|
8207
|
+
params: {
|
|
8208
|
+
type: "object",
|
|
8209
|
+
properties: {
|
|
8210
|
+
id: { type: "string", description: "The path." },
|
|
8211
|
+
index: { type: "integer", minimum: 0 },
|
|
8212
|
+
t: { type: "number", minimum: 0, maximum: 1 },
|
|
8213
|
+
left: { type: "number", minimum: 0 },
|
|
8214
|
+
right: { type: "number", minimum: 0 }
|
|
8215
|
+
},
|
|
8216
|
+
required: ["id"]
|
|
8217
|
+
},
|
|
8218
|
+
undoable: true,
|
|
8219
|
+
run: (_ctx, params) => {
|
|
8220
|
+
if (!params?.id) fail("Pass the path `id`.");
|
|
8221
|
+
check(controller.setWidthPoint({ nodeId: params.id, index: params.index ?? null }, { t: params.t, left: params.left, right: params.right }));
|
|
8222
|
+
const node = controller.getNode(params.id);
|
|
8223
|
+
return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
|
|
8224
|
+
}
|
|
8225
|
+
},
|
|
7967
8226
|
{
|
|
7968
8227
|
...base,
|
|
7969
8228
|
id: `${prefix}outlineStroke`,
|