@inditextech/weave-sdk 5.7.0 → 5.8.0-SNAPSHOT.613.1
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/dist/lab.d.ts +289 -0
- package/dist/lab.d.ts.map +1 -0
- package/dist/lab.js +1438 -0
- package/dist/lab.js.map +1 -0
- package/dist/lab.stats.html +5305 -0
- package/dist/sdk.js +3250 -174
- package/dist/sdk.node.js +3250 -174
- package/dist/sdk.node.stats.html +1 -1
- package/dist/sdk.stats.html +1 -1
- package/dist/types.d.ts +610 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3250 -174
- package/dist/types.js.map +1 -1
- package/dist/types.stats.html +1 -1
- package/package.json +8 -3
package/dist/types.js
CHANGED
|
@@ -9579,7 +9579,7 @@ var WeaveRegisterManager = class {
|
|
|
9579
9579
|
};
|
|
9580
9580
|
//#endregion
|
|
9581
9581
|
//#region package.json
|
|
9582
|
-
var version = "5.
|
|
9582
|
+
var version = "5.8.0-SNAPSHOT.613.1";
|
|
9583
9583
|
//#endregion
|
|
9584
9584
|
//#region src/managers/setup.ts
|
|
9585
9585
|
var WeaveSetupManager = class {
|
|
@@ -12562,6 +12562,12 @@ function isSafeLink(value) {
|
|
|
12562
12562
|
return false;
|
|
12563
12563
|
}
|
|
12564
12564
|
}
|
|
12565
|
+
/** Marks an empty block carries: normalized and never a link. */
|
|
12566
|
+
function emptyBlockMarks(m) {
|
|
12567
|
+
if (!m) return void 0;
|
|
12568
|
+
const { link: _link, ...rest } = m;
|
|
12569
|
+
return normalizeMarks(rest);
|
|
12570
|
+
}
|
|
12565
12571
|
/** Block fields that describe structure (not content). */
|
|
12566
12572
|
function blockFields(block, withStart) {
|
|
12567
12573
|
const out = {};
|
|
@@ -12639,6 +12645,76 @@ function sliceRuns(runs, from, to) {
|
|
|
12639
12645
|
}
|
|
12640
12646
|
return out;
|
|
12641
12647
|
}
|
|
12648
|
+
/** Blocks covering the global range [start, end), keeping structure and marks. */
|
|
12649
|
+
function sliceBlocks(blocks, start, end) {
|
|
12650
|
+
const out = [];
|
|
12651
|
+
let pos = 0;
|
|
12652
|
+
for (const block of blocks) {
|
|
12653
|
+
const len = blockText(block).length;
|
|
12654
|
+
const bStart = pos;
|
|
12655
|
+
const bEnd = pos + len;
|
|
12656
|
+
pos = bEnd + 1;
|
|
12657
|
+
if (bEnd < start || bStart > end) continue;
|
|
12658
|
+
const runs = normalizeRuns(sliceRuns(block.runs, Math.max(start, bStart) - bStart, end - bStart));
|
|
12659
|
+
out.push({
|
|
12660
|
+
...blockFields(block, true),
|
|
12661
|
+
runs,
|
|
12662
|
+
...runs.length === 0 && block.m ? { m: block.m } : {}
|
|
12663
|
+
});
|
|
12664
|
+
}
|
|
12665
|
+
return out;
|
|
12666
|
+
}
|
|
12667
|
+
/**
|
|
12668
|
+
* Replace the global range [start, end) with a fragment of blocks. A single
|
|
12669
|
+
* block fragment merges inline; otherwise first/last fragment blocks merge
|
|
12670
|
+
* with the text before/after the range.
|
|
12671
|
+
*/
|
|
12672
|
+
function insertBlocks(blocks, start, end, fragment) {
|
|
12673
|
+
if (fragment.length === 0) return blocks;
|
|
12674
|
+
const total = blocksToText(blocks).length;
|
|
12675
|
+
start = Math.max(0, Math.min(start, total));
|
|
12676
|
+
end = Math.max(start, Math.min(end, total));
|
|
12677
|
+
const head = sliceBlocks(blocks, 0, start);
|
|
12678
|
+
const tail = sliceBlocks(blocks, end, total);
|
|
12679
|
+
const before = head.pop() ?? { runs: [] };
|
|
12680
|
+
const after = tail.shift() ?? { runs: [] };
|
|
12681
|
+
const first = fragment[0];
|
|
12682
|
+
const last = fragment[fragment.length - 1];
|
|
12683
|
+
const finish = (b) => {
|
|
12684
|
+
const runs = normalizeRuns(b.runs);
|
|
12685
|
+
const { m: _m, ...rest } = b;
|
|
12686
|
+
return {
|
|
12687
|
+
...rest,
|
|
12688
|
+
runs,
|
|
12689
|
+
...runs.length === 0 && b.m ? { m: b.m } : {}
|
|
12690
|
+
};
|
|
12691
|
+
};
|
|
12692
|
+
if (fragment.length === 1) return [
|
|
12693
|
+
...head,
|
|
12694
|
+
finish({
|
|
12695
|
+
...before,
|
|
12696
|
+
runs: [
|
|
12697
|
+
...before.runs,
|
|
12698
|
+
...first.runs,
|
|
12699
|
+
...after.runs
|
|
12700
|
+
]
|
|
12701
|
+
}),
|
|
12702
|
+
...tail
|
|
12703
|
+
];
|
|
12704
|
+
return [
|
|
12705
|
+
...head,
|
|
12706
|
+
finish({
|
|
12707
|
+
...before,
|
|
12708
|
+
runs: [...before.runs, ...first.runs]
|
|
12709
|
+
}),
|
|
12710
|
+
...fragment.slice(1, -1).map(finish),
|
|
12711
|
+
finish({
|
|
12712
|
+
...last,
|
|
12713
|
+
runs: [...last.runs, ...after.runs]
|
|
12714
|
+
}),
|
|
12715
|
+
...tail
|
|
12716
|
+
];
|
|
12717
|
+
}
|
|
12642
12718
|
/** Marks of the run at (or immediately before) a local offset. */
|
|
12643
12719
|
function marksAt(runs, offset) {
|
|
12644
12720
|
let pos = 0;
|
|
@@ -12679,7 +12755,7 @@ function replaceRange(blocks, start, end, insert, insertMarks) {
|
|
|
12679
12755
|
const local = start - bStart;
|
|
12680
12756
|
before = sliceRuns(block.runs, 0, local);
|
|
12681
12757
|
startBlock = block;
|
|
12682
|
-
inheritedMarks = marksAt(block.runs, local);
|
|
12758
|
+
inheritedMarks = len === 0 ? block.m : marksAt(block.runs, local);
|
|
12683
12759
|
}
|
|
12684
12760
|
if (end >= bStart && end <= bEnd) after = sliceRuns(block.runs, end - bStart, len);
|
|
12685
12761
|
}
|
|
@@ -12695,9 +12771,12 @@ function replaceRange(blocks, start, end, insert, insertMarks) {
|
|
|
12695
12771
|
let runs = mk(line);
|
|
12696
12772
|
if (idx === 0) runs = [...before, ...runs];
|
|
12697
12773
|
if (idx === lines.length - 1) runs = [...runs, ...after];
|
|
12774
|
+
const normalized = normalizeRuns(runs);
|
|
12775
|
+
const carried = emptyBlockMarks(inheritedMarks);
|
|
12698
12776
|
middle.push({
|
|
12699
12777
|
...blockFields(startBlock, idx === 0),
|
|
12700
|
-
runs:
|
|
12778
|
+
runs: normalized,
|
|
12779
|
+
...normalized.length === 0 && carried ? { m: carried } : {}
|
|
12701
12780
|
});
|
|
12702
12781
|
});
|
|
12703
12782
|
return [
|
|
@@ -12761,7 +12840,11 @@ function blockIndicesInRange(blocks, start, end) {
|
|
|
12761
12840
|
}
|
|
12762
12841
|
/** Marks that typing at a collapsed offset would inherit. */
|
|
12763
12842
|
function marksAtOffset(blocks, offset) {
|
|
12764
|
-
for (const s of blockSpans(blocks)) if (offset >= s.start && offset <= s.end)
|
|
12843
|
+
for (const s of blockSpans(blocks)) if (offset >= s.start && offset <= s.end) {
|
|
12844
|
+
const block = blocks[s.index];
|
|
12845
|
+
if (block.runs.length === 0) return cleanMarks(block.m);
|
|
12846
|
+
return cleanMarks(marksAt(block.runs, offset - s.start));
|
|
12847
|
+
}
|
|
12765
12848
|
}
|
|
12766
12849
|
function mapRunsInRange(block, from, to, fn) {
|
|
12767
12850
|
const runs = [];
|
|
@@ -12794,6 +12877,26 @@ function mapRunsInRange(block, from, to, fn) {
|
|
|
12794
12877
|
runs: normalizeRuns(runs)
|
|
12795
12878
|
};
|
|
12796
12879
|
}
|
|
12880
|
+
function withEmptyMark(block, key, value) {
|
|
12881
|
+
const { m: _m, ...rest } = block;
|
|
12882
|
+
const m = normalizeMarks({
|
|
12883
|
+
...block.m,
|
|
12884
|
+
[key]: value
|
|
12885
|
+
});
|
|
12886
|
+
return m ? {
|
|
12887
|
+
...rest,
|
|
12888
|
+
m
|
|
12889
|
+
} : rest;
|
|
12890
|
+
}
|
|
12891
|
+
/**
|
|
12892
|
+
* Set (or remove with `undefined`) one mark on the empty block holding the
|
|
12893
|
+
* collapsed offset. Other blocks are returned untouched.
|
|
12894
|
+
*/
|
|
12895
|
+
function applyMarkToEmptyBlock(blocks, offset, key, value) {
|
|
12896
|
+
const span = blockSpans(blocks).find((s) => offset >= s.start && offset <= s.end);
|
|
12897
|
+
if (!span || blocks[span.index].runs.length > 0) return blocks;
|
|
12898
|
+
return blocks.map((b, i) => i === span.index ? withEmptyMark(b, key, value) : b);
|
|
12899
|
+
}
|
|
12797
12900
|
/** Set (or remove with `undefined`) one mark over the global range. */
|
|
12798
12901
|
function applyMark(blocks, start, end, key, value) {
|
|
12799
12902
|
if (start >= end) return blocks;
|
|
@@ -12806,6 +12909,7 @@ function applyMark(blocks, start, end, key, value) {
|
|
|
12806
12909
|
return blocks.map((block, i) => {
|
|
12807
12910
|
const s = spans[i];
|
|
12808
12911
|
if (s.end <= start || s.start >= end) return block;
|
|
12912
|
+
if (block.runs.length === 0) return withEmptyMark(block, key, value);
|
|
12809
12913
|
return mapRunsInRange(block, Math.max(start, s.start) - s.start, Math.min(end, s.end) - s.start, (m) => normalizeMarks({
|
|
12810
12914
|
...m,
|
|
12811
12915
|
[key]: value
|
|
@@ -13031,6 +13135,44 @@ function alignQuery(blocks, start, end) {
|
|
|
13031
13135
|
value: set[0]
|
|
13032
13136
|
};
|
|
13033
13137
|
}
|
|
13138
|
+
const WEAVE_TEXT_MIME = "application/x-weave-text+json";
|
|
13139
|
+
/** Defensive parse of a clipboard payload; undefined when invalid. */
|
|
13140
|
+
function parseWeaveClipboard(raw) {
|
|
13141
|
+
if (!raw) return void 0;
|
|
13142
|
+
try {
|
|
13143
|
+
const data = JSON.parse(raw);
|
|
13144
|
+
if (data?.v !== 1 || !Array.isArray(data.blocks) || !data.blocks.length) return;
|
|
13145
|
+
const out = [];
|
|
13146
|
+
for (const b of data.blocks.slice(0, 5e3)) {
|
|
13147
|
+
if (!b || !Array.isArray(b.runs)) return void 0;
|
|
13148
|
+
const runs = [];
|
|
13149
|
+
for (const r of b.runs) {
|
|
13150
|
+
if (typeof r?.t !== "string") return void 0;
|
|
13151
|
+
const m = cleanMarks(r.m && typeof r.m === "object" ? { ...r.m } : void 0);
|
|
13152
|
+
if (m && "link" in m && !isSafeLink(m.link)) delete m.link;
|
|
13153
|
+
runs.push({
|
|
13154
|
+
t: r.t,
|
|
13155
|
+
...m && Object.keys(m).length ? { m } : {}
|
|
13156
|
+
});
|
|
13157
|
+
}
|
|
13158
|
+
const block = { runs: normalizeRuns(runs) };
|
|
13159
|
+
if (b.kind === "ul" || b.kind === "ol") block.kind = b.kind;
|
|
13160
|
+
if (Number.isInteger(b.depth) && b.depth > 0) block.depth = b.depth;
|
|
13161
|
+
if (Number.isInteger(b.start)) block.start = b.start;
|
|
13162
|
+
if ([
|
|
13163
|
+
"left",
|
|
13164
|
+
"center",
|
|
13165
|
+
"right"
|
|
13166
|
+
].includes(b.align)) block.align = b.align;
|
|
13167
|
+
const em = cleanMarks(b.m && typeof b.m === "object" ? b.m : void 0);
|
|
13168
|
+
if (!block.runs.length && em) block.m = em;
|
|
13169
|
+
out.push(block);
|
|
13170
|
+
}
|
|
13171
|
+
return out;
|
|
13172
|
+
} catch {
|
|
13173
|
+
return;
|
|
13174
|
+
}
|
|
13175
|
+
}
|
|
13034
13176
|
//#endregion
|
|
13035
13177
|
//#region src/nodes/text-v2/layout/engine.ts
|
|
13036
13178
|
const DEFAULT_INDENT_STEP = 1.5;
|
|
@@ -13179,7 +13321,7 @@ var TextLayoutEngine = class {
|
|
|
13179
13321
|
return Math.min(wanted, limit);
|
|
13180
13322
|
}
|
|
13181
13323
|
sigFor(block, style, fixedWidth, maxWidth, indent) {
|
|
13182
|
-
return `${fontString(style)}|${style.lineHeight}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth - indent : "auto"}|${JSON.stringify(block.runs)}`;
|
|
13324
|
+
return `${fontString(style)}|${style.lineHeight}|${style.letterSpacing}|${style.wrap}|${fixedWidth ? maxWidth - indent : "auto"}|${JSON.stringify(block.runs)}|${block.runs.length === 0 ? JSON.stringify(block.m ?? null) : ""}`;
|
|
13183
13325
|
}
|
|
13184
13326
|
/**
|
|
13185
13327
|
* Pre-compute paragraph layouts for `blocks` within a time budget so a
|
|
@@ -13276,7 +13418,7 @@ var TextLayoutEngine = class {
|
|
|
13276
13418
|
const startChar = entry.graphemes.slice(0, pl.startG).join("").length;
|
|
13277
13419
|
let marker;
|
|
13278
13420
|
if (first && opts.marker) {
|
|
13279
|
-
const m0 = block.runs[0]?.m;
|
|
13421
|
+
const m0 = block.runs[0]?.m ?? block.m;
|
|
13280
13422
|
const rs = runStyle(style, m0);
|
|
13281
13423
|
const stepPx = Math.min(indent, this.stepPx(style));
|
|
13282
13424
|
marker = {
|
|
@@ -13390,9 +13532,10 @@ var TextLayoutEngine = class {
|
|
|
13390
13532
|
desc = Math.max(desc, m.descent);
|
|
13391
13533
|
}
|
|
13392
13534
|
if (seen.size === 0) {
|
|
13393
|
-
|
|
13394
|
-
|
|
13395
|
-
|
|
13535
|
+
const rs = runStyle(style, entry.emptyMarks);
|
|
13536
|
+
size = rs.fontSize;
|
|
13537
|
+
height = (runLineHeight(entry.emptyMarks) ?? style.lineHeight) * size;
|
|
13538
|
+
const m = this.metrics(entry.emptyMarks ? fontString(rs) : baseFont, size);
|
|
13396
13539
|
asc = m.ascent;
|
|
13397
13540
|
desc = m.descent;
|
|
13398
13541
|
}
|
|
@@ -13412,6 +13555,7 @@ var TextLayoutEngine = class {
|
|
|
13412
13555
|
fonts,
|
|
13413
13556
|
lineHeights: block.runs.map((r) => runLineHeight(r.m) ?? style.lineHeight),
|
|
13414
13557
|
sizes,
|
|
13558
|
+
...block.runs.length === 0 && block.m ? { emptyMarks: block.m } : {},
|
|
13415
13559
|
lines: []
|
|
13416
13560
|
};
|
|
13417
13561
|
const n = graphemes.length;
|
|
@@ -14467,6 +14611,10 @@ var TextEditModel = class {
|
|
|
14467
14611
|
const { start, end } = this.getSelection();
|
|
14468
14612
|
return this.getText().slice(start, end);
|
|
14469
14613
|
}
|
|
14614
|
+
getSelectedBlocks() {
|
|
14615
|
+
const { start, end } = this.getSelection();
|
|
14616
|
+
return sliceBlocks(this.blocks, start, end);
|
|
14617
|
+
}
|
|
14470
14618
|
canUndo() {
|
|
14471
14619
|
return this.undoStack.length > 0;
|
|
14472
14620
|
}
|
|
@@ -14541,6 +14689,19 @@ var TextEditModel = class {
|
|
|
14541
14689
|
this.desiredX = null;
|
|
14542
14690
|
return true;
|
|
14543
14691
|
}
|
|
14692
|
+
/** Replace the selection with rich blocks; caret ends after them. */
|
|
14693
|
+
replaceSelectionWithBlocks(fragment, kind) {
|
|
14694
|
+
if (fragment.length === 0) return false;
|
|
14695
|
+
const { start, end } = this.getSelection();
|
|
14696
|
+
const insertedLen = blocksToText(fragment).length;
|
|
14697
|
+
this.record(kind, start);
|
|
14698
|
+
this.blocks = insertBlocks(this.blocks, start, end, fragment);
|
|
14699
|
+
this.storedMarks = null;
|
|
14700
|
+
this.anchor = this.focus = start + insertedLen;
|
|
14701
|
+
this.lastEditEnd = this.focus;
|
|
14702
|
+
this.desiredX = null;
|
|
14703
|
+
return true;
|
|
14704
|
+
}
|
|
14544
14705
|
replaceSelection(text, kind) {
|
|
14545
14706
|
const { start, end } = this.getSelection();
|
|
14546
14707
|
return this.replaceRange(start, end, text, kind);
|
|
@@ -14596,6 +14757,12 @@ var TextEditModel = class {
|
|
|
14596
14757
|
if (value === void 0 || value === false) delete base[key];
|
|
14597
14758
|
else base[key] = value;
|
|
14598
14759
|
this.storedMarks = cleanMarks(base) ?? {};
|
|
14760
|
+
const next = applyMarkToEmptyBlock(this.blocks, start, key, value);
|
|
14761
|
+
if (next !== this.blocks) {
|
|
14762
|
+
const stored = this.storedMarks;
|
|
14763
|
+
this.commitFormat(next);
|
|
14764
|
+
this.storedMarks = stored;
|
|
14765
|
+
}
|
|
14599
14766
|
return true;
|
|
14600
14767
|
}
|
|
14601
14768
|
return this.commitFormat(applyMark(this.blocks, start, end, key, value));
|
|
@@ -14667,7 +14834,7 @@ var TextEditModel = class {
|
|
|
14667
14834
|
function outdentToPlain(blocks, index) {
|
|
14668
14835
|
return blocks.map((b, i) => {
|
|
14669
14836
|
if (i !== index) return b;
|
|
14670
|
-
const { kind: _k, depth: _d, start: _s, ...rest } = b;
|
|
14837
|
+
const { kind: _k, depth: _d, start: _s, m: _m, ...rest } = b;
|
|
14671
14838
|
return rest;
|
|
14672
14839
|
});
|
|
14673
14840
|
}
|
|
@@ -14708,6 +14875,8 @@ var TextEditSession = class {
|
|
|
14708
14875
|
ta.addEventListener("beforeinput", this.onBeforeInput, opts);
|
|
14709
14876
|
ta.addEventListener("input", this.onInput, opts);
|
|
14710
14877
|
ta.addEventListener("paste", this.onPaste, opts);
|
|
14878
|
+
ta.addEventListener("copy", this.onCopy, opts);
|
|
14879
|
+
ta.addEventListener("cut", this.onCut, opts);
|
|
14711
14880
|
ta.addEventListener("compositionstart", this.onCompositionStart, opts);
|
|
14712
14881
|
ta.addEventListener("compositionend", this.onCompositionEnd, opts);
|
|
14713
14882
|
ta.addEventListener("select", this.syncFromTextarea, opts);
|
|
@@ -14954,9 +15123,43 @@ var TextEditSession = class {
|
|
|
14954
15123
|
if (inputType === "insertFromPaste") return "paste";
|
|
14955
15124
|
return "type";
|
|
14956
15125
|
}
|
|
15126
|
+
writeRich(e) {
|
|
15127
|
+
const { start, end } = this.model.getSelection();
|
|
15128
|
+
if (start === end || !this.host.richText || !e.clipboardData) return false;
|
|
15129
|
+
e.clipboardData.setData("text/plain", this.model.getSelectedText());
|
|
15130
|
+
e.clipboardData.setData(WEAVE_TEXT_MIME, JSON.stringify({
|
|
15131
|
+
v: 1,
|
|
15132
|
+
blocks: this.model.getSelectedBlocks()
|
|
15133
|
+
}));
|
|
15134
|
+
e.preventDefault();
|
|
15135
|
+
return true;
|
|
15136
|
+
}
|
|
15137
|
+
onCopy = (e) => {
|
|
15138
|
+
e.stopPropagation();
|
|
15139
|
+
this.writeRich(e);
|
|
15140
|
+
};
|
|
15141
|
+
onCut = (e) => {
|
|
15142
|
+
e.stopPropagation();
|
|
15143
|
+
if (!this.writeRich(e)) return;
|
|
15144
|
+
if (this.model.replaceSelection("", "delete")) {
|
|
15145
|
+
this.syncTextarea();
|
|
15146
|
+
this.afterContentChange(0);
|
|
15147
|
+
}
|
|
15148
|
+
};
|
|
14957
15149
|
onPaste = (e) => {
|
|
14958
15150
|
e.stopPropagation();
|
|
14959
15151
|
e.preventDefault();
|
|
15152
|
+
if (this.host.richText) {
|
|
15153
|
+
const blocks = parseWeaveClipboard(e.clipboardData?.getData(WEAVE_TEXT_MIME));
|
|
15154
|
+
if (blocks) {
|
|
15155
|
+
const len = blocksToText(blocks).length;
|
|
15156
|
+
if (this.model.replaceSelectionWithBlocks(blocks, "paste")) {
|
|
15157
|
+
this.syncTextarea();
|
|
15158
|
+
this.afterContentChange(len);
|
|
15159
|
+
}
|
|
15160
|
+
return;
|
|
15161
|
+
}
|
|
15162
|
+
}
|
|
14960
15163
|
const raw = e.clipboardData?.getData("text/plain") ?? "";
|
|
14961
15164
|
if (raw === "") return;
|
|
14962
15165
|
this.insertText(raw.replace(/\r\n?/g, "\n"), "paste");
|
|
@@ -17601,7 +17804,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
17601
17804
|
},
|
|
17602
17805
|
signal: this.instance.getEventsController().signal,
|
|
17603
17806
|
onChange: (blocks) => this.checkSize(shape, blocks),
|
|
17604
|
-
onRequestExit: () => this.exitEditMode(shape)
|
|
17807
|
+
onRequestExit: (reason) => this.exitEditMode(shape, reason)
|
|
17605
17808
|
}, shape.id());
|
|
17606
17809
|
shape.setAttr("cancelEditMode", () => this.exitEditMode(shape));
|
|
17607
17810
|
stage.mode(WEAVE_STAGE_TEXT_EDITION_MODE);
|
|
@@ -17624,7 +17827,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
17624
17827
|
});
|
|
17625
17828
|
} else if (size <= threshold) this.sizeWarned = false;
|
|
17626
17829
|
}
|
|
17627
|
-
exitEditMode(shape) {
|
|
17830
|
+
exitEditMode(shape, reason) {
|
|
17628
17831
|
const session = this.session;
|
|
17629
17832
|
if (!session || !this.editing) return;
|
|
17630
17833
|
clearTimeout(this.selectionTimer);
|
|
@@ -17642,7 +17845,7 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
17642
17845
|
const selectionPlugin = this.instance.getPlugin("nodesSelection");
|
|
17643
17846
|
if (selectionPlugin) {
|
|
17644
17847
|
this.instance.enablePlugin("nodesSelection");
|
|
17645
|
-
selectionPlugin.setSelectedNodes([liveNode]);
|
|
17848
|
+
selectionPlugin.setSelectedNodes(reason === "outside" ? [] : [liveNode]);
|
|
17646
17849
|
this.instance.triggerAction(SELECTION_TOOL_ACTION_NAME);
|
|
17647
17850
|
}
|
|
17648
17851
|
stage.container().tabIndex = 1;
|
|
@@ -17657,12 +17860,33 @@ var WeaveTextV2Node = class extends WeaveNode {
|
|
|
17657
17860
|
const serialized = this.serialize(live);
|
|
17658
17861
|
if (live.text() !== "") {
|
|
17659
17862
|
const container = live.getParent();
|
|
17660
|
-
|
|
17863
|
+
this.swapOnRendered(live);
|
|
17661
17864
|
this.instance.addNode(serialized, container?.getAttrs().id);
|
|
17662
17865
|
} else live.destroy();
|
|
17663
17866
|
} else if (changed) this.instance.updateNode(this.serialize(live));
|
|
17664
17867
|
this.createNode = false;
|
|
17665
17868
|
}
|
|
17869
|
+
/** Destroy `temp` once the real node with the same id is in the tree. */
|
|
17870
|
+
swapOnRendered(temp) {
|
|
17871
|
+
const id = temp.id();
|
|
17872
|
+
const timer = {};
|
|
17873
|
+
const finish = (real) => {
|
|
17874
|
+
clearTimeout(timer.id);
|
|
17875
|
+
this.instance.removeEventListener("onNodeRenderedAdded", onRendered);
|
|
17876
|
+
temp.destroy();
|
|
17877
|
+
if (!real) return;
|
|
17878
|
+
const selection = this.instance.getPlugin("nodesSelection");
|
|
17879
|
+
const selected = selection?.getSelectedNodes?.() ?? [];
|
|
17880
|
+
if (selection && selected.some((n) => n.id() === id)) selection.setSelectedNodes([real]);
|
|
17881
|
+
real.getLayer()?.batchDraw();
|
|
17882
|
+
};
|
|
17883
|
+
const onRendered = (node) => {
|
|
17884
|
+
if (node === temp || node.id() !== id || !node.getParent()) return;
|
|
17885
|
+
finish(node);
|
|
17886
|
+
};
|
|
17887
|
+
this.instance.addEventListener("onNodeRenderedAdded", onRendered);
|
|
17888
|
+
timer.id = setTimeout(() => finish(), 2e3);
|
|
17889
|
+
}
|
|
17666
17890
|
setupTransform(text) {
|
|
17667
17891
|
let actualAnchor = void 0;
|
|
17668
17892
|
const isSide = () => SIDE_ANCHOR_SET.has(actualAnchor ?? "");
|
|
@@ -21929,6 +22153,27 @@ var WeaveFrameNode = class extends WeaveNode {
|
|
|
21929
22153
|
//#endregion
|
|
21930
22154
|
//#region src/nodes/stroke/constants.ts
|
|
21931
22155
|
const WEAVE_STROKE_NODE_TYPE = "stroke";
|
|
22156
|
+
/**
|
|
22157
|
+
* The look equivalent to the rendering of strokes that have no look: the
|
|
22158
|
+
* default of every look setting.
|
|
22159
|
+
*/
|
|
22160
|
+
const WEAVE_STROKE_LEGACY_LOOK = {
|
|
22161
|
+
version: 1,
|
|
22162
|
+
outline: "legacy",
|
|
22163
|
+
curve: "uniform",
|
|
22164
|
+
pressureInterp: "catmullRom",
|
|
22165
|
+
zoomAware: false,
|
|
22166
|
+
spacingPx: 2,
|
|
22167
|
+
edgePx: 1,
|
|
22168
|
+
caps: "separate",
|
|
22169
|
+
width: {
|
|
22170
|
+
min: 0,
|
|
22171
|
+
max: 1,
|
|
22172
|
+
gamma: 1,
|
|
22173
|
+
taperStart: 0,
|
|
22174
|
+
taperEnd: 0
|
|
22175
|
+
}
|
|
22176
|
+
};
|
|
21932
22177
|
const WEAVE_STROKE_NODE_DEFAULT_CONFIG = {
|
|
21933
22178
|
splineResolution: 8,
|
|
21934
22179
|
splineTargetEdge: 1,
|
|
@@ -21937,9 +22182,380 @@ const WEAVE_STROKE_NODE_DEFAULT_CONFIG = {
|
|
|
21937
22182
|
isEraser: false
|
|
21938
22183
|
};
|
|
21939
22184
|
//#endregion
|
|
22185
|
+
//#region src/nodes/stroke/render/curve.ts
|
|
22186
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
22187
|
+
/** Interval between two knots: the square root of the distance (alpha 0.5). */
|
|
22188
|
+
const knotStep = (a, b) => Math.max(Math.sqrt(Math.hypot(b.x - a.x, b.y - a.y)), 1e-6);
|
|
22189
|
+
/** Barry-Goldman evaluation of the centripetal Catmull-Rom curve at `u` in 0..1 of the middle span. */
|
|
22190
|
+
function evaluate(w, u) {
|
|
22191
|
+
const { p, t } = w;
|
|
22192
|
+
const at = lerp(t[1], t[2], u);
|
|
22193
|
+
const blend = (channel) => {
|
|
22194
|
+
const a1 = lerp(p[0][channel], p[1][channel], (at - t[0]) / (t[1] - t[0]));
|
|
22195
|
+
const a2 = lerp(p[1][channel], p[2][channel], (at - t[1]) / (t[2] - t[1]));
|
|
22196
|
+
const a3 = lerp(p[2][channel], p[3][channel], (at - t[2]) / (t[3] - t[2]));
|
|
22197
|
+
const b1 = lerp(a1, a2, (at - t[0]) / (t[2] - t[0]));
|
|
22198
|
+
const b2 = lerp(a2, a3, (at - t[1]) / (t[3] - t[1]));
|
|
22199
|
+
return lerp(b1, b2, (at - t[1]) / (t[2] - t[1]));
|
|
22200
|
+
};
|
|
22201
|
+
return {
|
|
22202
|
+
x: blend("x"),
|
|
22203
|
+
y: blend("y"),
|
|
22204
|
+
pressure: blend("pressure")
|
|
22205
|
+
};
|
|
22206
|
+
}
|
|
22207
|
+
function distanceToSegment(p, a, b) {
|
|
22208
|
+
const dx = b.x - a.x;
|
|
22209
|
+
const dy = b.y - a.y;
|
|
22210
|
+
const l = dx * dx + dy * dy;
|
|
22211
|
+
const t = l ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / l)) : 0;
|
|
22212
|
+
return Math.hypot(p.x - (a.x + dx * t), p.y - (a.y + dy * t));
|
|
22213
|
+
}
|
|
22214
|
+
/**
|
|
22215
|
+
* Samples a centripetal Catmull-Rom curve through `pts`.
|
|
22216
|
+
*
|
|
22217
|
+
* The knot intervals follow the distance between points, so the curve does not
|
|
22218
|
+
* overshoot or form cusps when the points are unevenly spaced, which the
|
|
22219
|
+
* uniform parametrization does. Pressure is interpolated linearly between the
|
|
22220
|
+
* two points of a span, or smoothly but never beyond them, so the width of the
|
|
22221
|
+
* stroke cannot exceed the widest of its neighbouring widths.
|
|
22222
|
+
*/
|
|
22223
|
+
function getCentripetalPoints(pts, opts) {
|
|
22224
|
+
if (pts.length < 2) return [...pts];
|
|
22225
|
+
const out = [];
|
|
22226
|
+
for (let i = -1; i < pts.length - 2; i++) {
|
|
22227
|
+
const p0 = pts[Math.max(i, 0)];
|
|
22228
|
+
const p1 = pts[i + 1];
|
|
22229
|
+
const p2 = pts[i + 2];
|
|
22230
|
+
const p3 = pts[Math.min(i + 3, pts.length - 1)];
|
|
22231
|
+
const t1 = knotStep(p0, p1);
|
|
22232
|
+
const t2 = t1 + knotStep(p1, p2);
|
|
22233
|
+
const t3 = t2 + knotStep(p2, p3);
|
|
22234
|
+
const w = {
|
|
22235
|
+
p: [
|
|
22236
|
+
p0,
|
|
22237
|
+
p1,
|
|
22238
|
+
p2,
|
|
22239
|
+
p3
|
|
22240
|
+
],
|
|
22241
|
+
t: [
|
|
22242
|
+
0,
|
|
22243
|
+
t1,
|
|
22244
|
+
t2,
|
|
22245
|
+
t3
|
|
22246
|
+
]
|
|
22247
|
+
};
|
|
22248
|
+
let deviation = 0;
|
|
22249
|
+
for (const probe of [
|
|
22250
|
+
.25,
|
|
22251
|
+
.5,
|
|
22252
|
+
.75
|
|
22253
|
+
]) deviation = Math.max(deviation, distanceToSegment(evaluate(w, probe), p1, p2));
|
|
22254
|
+
const tolerance = Math.max(opts.edge * .1, 1e-6);
|
|
22255
|
+
const steps = deviation < opts.flatness ? 1 : Math.min(Math.max(Math.ceil(Math.sqrt(deviation / tolerance)), opts.minSteps), opts.maxSteps);
|
|
22256
|
+
const lo = Math.min(p1.pressure, p2.pressure);
|
|
22257
|
+
const hi = Math.max(p1.pressure, p2.pressure);
|
|
22258
|
+
for (let s = 0; s < steps; s++) {
|
|
22259
|
+
const u = s / steps;
|
|
22260
|
+
const q = evaluate(w, u);
|
|
22261
|
+
q.pressure = opts.pressure === "linear" ? lerp(p1.pressure, p2.pressure, u) : Math.min(hi, Math.max(lo, q.pressure));
|
|
22262
|
+
out.push(q);
|
|
22263
|
+
}
|
|
22264
|
+
}
|
|
22265
|
+
out.push(pts[pts.length - 1]);
|
|
22266
|
+
return out;
|
|
22267
|
+
}
|
|
22268
|
+
//#endregion
|
|
22269
|
+
//#region src/nodes/stroke/render/look.ts
|
|
22270
|
+
const OUTLINES = [
|
|
22271
|
+
"legacy",
|
|
22272
|
+
"round",
|
|
22273
|
+
"freehand"
|
|
22274
|
+
];
|
|
22275
|
+
const CURVES = [
|
|
22276
|
+
"uniform",
|
|
22277
|
+
"centripetal",
|
|
22278
|
+
"none"
|
|
22279
|
+
];
|
|
22280
|
+
const PRESSURE_INTERPS = ["catmullRom", "linear"];
|
|
22281
|
+
const CAPS = ["separate", "merged"];
|
|
22282
|
+
const isObject$2 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
22283
|
+
const pick = (v, allowed, fallback) => typeof v === "string" && allowed.includes(v) ? v : fallback;
|
|
22284
|
+
/** A finite number clamped to a range, or the fallback. */
|
|
22285
|
+
const num = (v, min, max, fallback) => typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : fallback;
|
|
22286
|
+
/**
|
|
22287
|
+
* Reads the look saved on a stroke. Returns `null` when the stroke has none or
|
|
22288
|
+
* its look is of a version this release does not understand, so the stroke is
|
|
22289
|
+
* drawn as in releases that did not know looks. Missing or invalid fields fall
|
|
22290
|
+
* back to the legacy values and out-of-range numbers are clamped, so a stored
|
|
22291
|
+
* look can never make the renderer misbehave.
|
|
22292
|
+
*/
|
|
22293
|
+
function readStrokeLook(raw) {
|
|
22294
|
+
if (!isObject$2(raw) || raw["version"] !== WEAVE_STROKE_LEGACY_LOOK.version) return null;
|
|
22295
|
+
const base = WEAVE_STROKE_LEGACY_LOOK;
|
|
22296
|
+
const width = isObject$2(raw["width"]) ? raw["width"] : {};
|
|
22297
|
+
return {
|
|
22298
|
+
version: 1,
|
|
22299
|
+
outline: pick(raw["outline"], OUTLINES, base.outline),
|
|
22300
|
+
curve: pick(raw["curve"], CURVES, base.curve),
|
|
22301
|
+
pressureInterp: pick(raw["pressureInterp"], PRESSURE_INTERPS, base.pressureInterp),
|
|
22302
|
+
zoomAware: typeof raw["zoomAware"] === "boolean" ? raw["zoomAware"] : base.zoomAware,
|
|
22303
|
+
spacingPx: num(raw["spacingPx"], .5, 10, base.spacingPx),
|
|
22304
|
+
edgePx: num(raw["edgePx"], .25, 5, base.edgePx),
|
|
22305
|
+
caps: pick(raw["caps"], CAPS, base.caps),
|
|
22306
|
+
width: {
|
|
22307
|
+
min: num(width["min"], 0, 1, base.width.min),
|
|
22308
|
+
max: num(width["max"], 0, 1, base.width.max),
|
|
22309
|
+
gamma: num(width["gamma"], .2, 3, base.width.gamma),
|
|
22310
|
+
taperStart: num(width["taperStart"], 0, 200, base.width.taperStart),
|
|
22311
|
+
taperEnd: num(width["taperEnd"], 0, 200, base.width.taperEnd)
|
|
22312
|
+
}
|
|
22313
|
+
};
|
|
22314
|
+
}
|
|
22315
|
+
/** Quadratic ease-out: 0 at the tip, 1 once the taper distance is covered. */
|
|
22316
|
+
const easeOut = (t) => t * (2 - t);
|
|
22317
|
+
/** Lowest half-width drawn, in canvas units, so a stroke never vanishes. */
|
|
22318
|
+
const MIN_HALF_WIDTH = .5;
|
|
22319
|
+
/**
|
|
22320
|
+
* Half of the drawn width at a point, in canvas units.
|
|
22321
|
+
*
|
|
22322
|
+
* The width is the stroke width times `min + (max - min) * pressure^gamma`,
|
|
22323
|
+
* narrowed by the start and end tapers. With the default width rule this is
|
|
22324
|
+
* `strokeWidth * pressure / 2`, the legacy width. `distanceFromStart` and
|
|
22325
|
+
* `distanceToEnd` are along the stroke, in the unit of the tapers.
|
|
22326
|
+
*/
|
|
22327
|
+
function halfWidthAt(look, strokeWidth, pressure, distanceFromStart, distanceToEnd) {
|
|
22328
|
+
const { min, max, gamma, taperStart, taperEnd } = look.width;
|
|
22329
|
+
const p = Math.min(1, Math.max(0, Number.isFinite(pressure) ? pressure : .5));
|
|
22330
|
+
let fraction = min + (max - min) * Math.pow(p, gamma);
|
|
22331
|
+
if (taperStart > 0 && distanceFromStart < taperStart) fraction *= easeOut(Math.max(0, distanceFromStart) / taperStart);
|
|
22332
|
+
if (taperEnd > 0 && distanceToEnd < taperEnd) fraction *= easeOut(Math.max(0, distanceToEnd) / taperEnd);
|
|
22333
|
+
return Math.max(strokeWidth * fraction / 2, MIN_HALF_WIDTH);
|
|
22334
|
+
}
|
|
22335
|
+
//#endregion
|
|
22336
|
+
//#region src/nodes/stroke/render/round-outline.ts
|
|
22337
|
+
/**
|
|
22338
|
+
* Drops points that the quads between their neighbours already cover. A point
|
|
22339
|
+
* is skipped only while every point skipped since the last kept one stays
|
|
22340
|
+
* within a hundredth of the radius of the chord that replaces them, in position
|
|
22341
|
+
* and in radius, so a vertex, a turn or a change of width is always kept. A gap
|
|
22342
|
+
* is also never wider than two radii. The first and last points are kept.
|
|
22343
|
+
*/
|
|
22344
|
+
function thinRoundPoints(pts) {
|
|
22345
|
+
if (pts.length <= 2) return pts;
|
|
22346
|
+
const out = [pts[0]];
|
|
22347
|
+
let anchor = 0;
|
|
22348
|
+
for (let i = 2; i < pts.length; i++) {
|
|
22349
|
+
const a = pts[anchor];
|
|
22350
|
+
const b = pts[i];
|
|
22351
|
+
const dx = b.x - a.x;
|
|
22352
|
+
const dy = b.y - a.y;
|
|
22353
|
+
const chord = Math.hypot(dx, dy);
|
|
22354
|
+
let keepPrevious = chord > 2 * Math.min(a.r, b.r) || i - anchor > 24;
|
|
22355
|
+
for (let j = anchor + 1; j < i && !keepPrevious; j++) {
|
|
22356
|
+
const p = pts[j];
|
|
22357
|
+
const t = chord > 0 ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / (chord * chord))) : 0;
|
|
22358
|
+
const off = Math.hypot(p.x - (a.x + dx * t), p.y - (a.y + dy * t));
|
|
22359
|
+
const tolerance = .01 * Math.min(a.r, b.r, p.r);
|
|
22360
|
+
if (off > tolerance || Math.abs(p.r - (a.r + (b.r - a.r) * t)) > tolerance) keepPrevious = true;
|
|
22361
|
+
}
|
|
22362
|
+
if (keepPrevious) {
|
|
22363
|
+
out.push(pts[i - 1]);
|
|
22364
|
+
anchor = i - 1;
|
|
22365
|
+
}
|
|
22366
|
+
}
|
|
22367
|
+
out.push(pts[pts.length - 1]);
|
|
22368
|
+
return out;
|
|
22369
|
+
}
|
|
22370
|
+
/** Largest gap, in screen pixels, a skipped disc may leave on the outer side of a bend. */
|
|
22371
|
+
const MAX_WEDGE_PX = .1;
|
|
22372
|
+
const angleDiff = (a, b) => {
|
|
22373
|
+
let d = Math.abs(a - b);
|
|
22374
|
+
if (d > Math.PI) d = 2 * Math.PI - d;
|
|
22375
|
+
return d;
|
|
22376
|
+
};
|
|
22377
|
+
/**
|
|
22378
|
+
* Which points need their disc. Between two neighbours the tangent quad covers
|
|
22379
|
+
* the stroke exactly; a disc only matters where the outline turns, to fill the
|
|
22380
|
+
* wedge on the outside of the bend. Where the turn is so slight that the
|
|
22381
|
+
* wedge would be thinner than a tenth of a screen pixel the disc is skipped,
|
|
22382
|
+
* which removes most arcs of a smooth stroke and makes it far cheaper to fill.
|
|
22383
|
+
*/
|
|
22384
|
+
function pointsNeedingDisc(pts, scale) {
|
|
22385
|
+
const need = new Array(pts.length).fill(false);
|
|
22386
|
+
if (pts.length === 0) return need;
|
|
22387
|
+
need[0] = true;
|
|
22388
|
+
need[pts.length - 1] = true;
|
|
22389
|
+
const dir = [0];
|
|
22390
|
+
const lean = [0];
|
|
22391
|
+
const valid = [false];
|
|
22392
|
+
for (let i = 1; i < pts.length; i++) {
|
|
22393
|
+
const p = pts[i];
|
|
22394
|
+
const q = pts[i - 1];
|
|
22395
|
+
const dx = p.x - q.x;
|
|
22396
|
+
const dy = p.y - q.y;
|
|
22397
|
+
const l = Math.hypot(dx, dy);
|
|
22398
|
+
const ok = l > Math.abs(p.r - q.r) && l >= 1e-9;
|
|
22399
|
+
valid.push(ok);
|
|
22400
|
+
dir.push(Math.atan2(dy, dx));
|
|
22401
|
+
lean.push(ok ? Math.asin(Math.max(-1, Math.min(1, (q.r - p.r) / l))) : 0);
|
|
22402
|
+
if (!ok) {
|
|
22403
|
+
need[i] = true;
|
|
22404
|
+
need[i - 1] = true;
|
|
22405
|
+
}
|
|
22406
|
+
}
|
|
22407
|
+
for (let i = 1; i < pts.length - 1; i++) {
|
|
22408
|
+
if (need[i]) continue;
|
|
22409
|
+
if (!valid[i] || !valid[i + 1]) {
|
|
22410
|
+
need[i] = true;
|
|
22411
|
+
continue;
|
|
22412
|
+
}
|
|
22413
|
+
const turn = angleDiff(dir[i], dir[i + 1]) + Math.abs(lean[i + 1] - lean[i]);
|
|
22414
|
+
const radiusPx = Math.max(pts[i].r * scale, 1e-6);
|
|
22415
|
+
if (turn > Math.sqrt(8 * MAX_WEDGE_PX / radiusPx)) need[i] = true;
|
|
22416
|
+
}
|
|
22417
|
+
return need;
|
|
22418
|
+
}
|
|
22419
|
+
/**
|
|
22420
|
+
* Appends the outline of a variable-width stroke to the current path: a disc at
|
|
22421
|
+
* every point and, between neighbours, the quad bounded by their two external
|
|
22422
|
+
* tangents. Every sub-path has the same winding, so a single non-zero fill is
|
|
22423
|
+
* the exact union: turns, reversals and ends are round by construction.
|
|
22424
|
+
*/
|
|
22425
|
+
function appendRoundPath(ctx, points, scale = 1) {
|
|
22426
|
+
const pts = thinRoundPoints(points);
|
|
22427
|
+
const need = pointsNeedingDisc(pts, scale);
|
|
22428
|
+
const polygon = (v) => {
|
|
22429
|
+
let area = 0;
|
|
22430
|
+
for (let k = 0; k < v.length; k++) {
|
|
22431
|
+
const [x1, y1] = v[k];
|
|
22432
|
+
const [x2, y2] = v[(k + 1) % v.length];
|
|
22433
|
+
area += x1 * y2 - x2 * y1;
|
|
22434
|
+
}
|
|
22435
|
+
const w = area < 0 ? [...v].reverse() : v;
|
|
22436
|
+
ctx.moveTo(w[0][0], w[0][1]);
|
|
22437
|
+
for (let k = 1; k < w.length; k++) ctx.lineTo(w[k][0], w[k][1]);
|
|
22438
|
+
ctx.closePath();
|
|
22439
|
+
};
|
|
22440
|
+
const tangents = (i) => {
|
|
22441
|
+
const p = pts[i];
|
|
22442
|
+
const q = pts[i - 1];
|
|
22443
|
+
const dx = p.x - q.x;
|
|
22444
|
+
const dy = p.y - q.y;
|
|
22445
|
+
const l = Math.hypot(dx, dy);
|
|
22446
|
+
if (l <= Math.abs(p.r - q.r) || l < 1e-9) return null;
|
|
22447
|
+
const ux = dx / l;
|
|
22448
|
+
const uy = dy / l;
|
|
22449
|
+
const k = (q.r - p.r) / l;
|
|
22450
|
+
const s = Math.sqrt(Math.max(0, 1 - k * k));
|
|
22451
|
+
return {
|
|
22452
|
+
a: [k * ux - s * uy, k * uy + s * ux],
|
|
22453
|
+
b: [k * ux + s * uy, k * uy - s * ux]
|
|
22454
|
+
};
|
|
22455
|
+
};
|
|
22456
|
+
let left = [];
|
|
22457
|
+
let right = [];
|
|
22458
|
+
let prev = null;
|
|
22459
|
+
const flush = () => {
|
|
22460
|
+
if (left.length >= 2) polygon([...left, ...right.reverse()]);
|
|
22461
|
+
left = [];
|
|
22462
|
+
right = [];
|
|
22463
|
+
prev = null;
|
|
22464
|
+
};
|
|
22465
|
+
for (let i = 1; i < pts.length; i++) {
|
|
22466
|
+
const p = pts[i];
|
|
22467
|
+
const q = pts[i - 1];
|
|
22468
|
+
const t = tangents(i);
|
|
22469
|
+
if (!t) {
|
|
22470
|
+
flush();
|
|
22471
|
+
continue;
|
|
22472
|
+
}
|
|
22473
|
+
if (!prev) {
|
|
22474
|
+
left = [[q.x + q.r * t.a[0], q.y + q.r * t.a[1]]];
|
|
22475
|
+
right = [[q.x + q.r * t.b[0], q.y + q.r * t.b[1]]];
|
|
22476
|
+
} else {
|
|
22477
|
+
const mitre = (u, v) => {
|
|
22478
|
+
const sx = u[0] + v[0];
|
|
22479
|
+
const sy = u[1] + v[1];
|
|
22480
|
+
const d = (sx * sx + sy * sy) / 2;
|
|
22481
|
+
return [sx / d * q.r, sy / d * q.r];
|
|
22482
|
+
};
|
|
22483
|
+
const ml = mitre(prev.a, t.a);
|
|
22484
|
+
const mr = mitre(prev.b, t.b);
|
|
22485
|
+
left.push([q.x + ml[0], q.y + ml[1]]);
|
|
22486
|
+
right.push([q.x + mr[0], q.y + mr[1]]);
|
|
22487
|
+
}
|
|
22488
|
+
prev = t;
|
|
22489
|
+
const last = i === pts.length - 1;
|
|
22490
|
+
const next = last ? null : tangents(i + 1);
|
|
22491
|
+
if (last || need[i] || !next) {
|
|
22492
|
+
left.push([p.x + p.r * t.a[0], p.y + p.r * t.a[1]]);
|
|
22493
|
+
right.push([p.x + p.r * t.b[0], p.y + p.r * t.b[1]]);
|
|
22494
|
+
flush();
|
|
22495
|
+
}
|
|
22496
|
+
}
|
|
22497
|
+
flush();
|
|
22498
|
+
for (let i = 0; i < pts.length; i++) {
|
|
22499
|
+
if (!need[i]) continue;
|
|
22500
|
+
const p = pts[i];
|
|
22501
|
+
ctx.moveTo(p.x + p.r, p.y);
|
|
22502
|
+
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2, false);
|
|
22503
|
+
}
|
|
22504
|
+
}
|
|
22505
|
+
/** Splits a centerline into the runs the dash pattern draws. */
|
|
22506
|
+
function splitRoundRuns(pts, dash) {
|
|
22507
|
+
if (!dash.length || dash.every((d) => !(d > 0))) return [pts];
|
|
22508
|
+
const runs = [];
|
|
22509
|
+
let index = 0;
|
|
22510
|
+
let on = true;
|
|
22511
|
+
let remaining = dash[0];
|
|
22512
|
+
let current = on ? [pts[0]] : [];
|
|
22513
|
+
for (let i = 1; i < pts.length; i++) {
|
|
22514
|
+
let a = pts[i - 1];
|
|
22515
|
+
const b = pts[i];
|
|
22516
|
+
let segment = Math.hypot(b.x - a.x, b.y - a.y);
|
|
22517
|
+
while (segment > remaining) {
|
|
22518
|
+
const t = remaining / segment;
|
|
22519
|
+
const cut = {
|
|
22520
|
+
x: a.x + (b.x - a.x) * t,
|
|
22521
|
+
y: a.y + (b.y - a.y) * t,
|
|
22522
|
+
r: a.r + (b.r - a.r) * t
|
|
22523
|
+
};
|
|
22524
|
+
if (on) {
|
|
22525
|
+
current.push(cut);
|
|
22526
|
+
runs.push(current);
|
|
22527
|
+
current = [];
|
|
22528
|
+
} else current = [cut];
|
|
22529
|
+
on = !on;
|
|
22530
|
+
index = (index + 1) % dash.length;
|
|
22531
|
+
segment -= remaining;
|
|
22532
|
+
remaining = dash[index];
|
|
22533
|
+
a = cut;
|
|
22534
|
+
if (!(remaining > 0)) remaining = Number.EPSILON;
|
|
22535
|
+
}
|
|
22536
|
+
remaining -= segment;
|
|
22537
|
+
if (on) current.push(b);
|
|
22538
|
+
}
|
|
22539
|
+
if (on && current.length > 0) runs.push(current);
|
|
22540
|
+
return runs.filter((r) => r.length > 0);
|
|
22541
|
+
}
|
|
22542
|
+
/**
|
|
22543
|
+
* Fills a round outline, once per dash run, so overlaps inside a run never
|
|
22544
|
+
* blend twice and a translucent colour stays uniform.
|
|
22545
|
+
*/
|
|
22546
|
+
function drawRoundOutline(ctx, points, color, dash = [], scale = 1) {
|
|
22547
|
+
for (const run of splitRoundRuns(points, dash)) {
|
|
22548
|
+
ctx.beginPath();
|
|
22549
|
+
ctx.fillStyle = color;
|
|
22550
|
+
appendRoundPath(ctx, run, scale);
|
|
22551
|
+
ctx.fill();
|
|
22552
|
+
}
|
|
22553
|
+
}
|
|
22554
|
+
//#endregion
|
|
21940
22555
|
//#region src/nodes/stroke/stroke.ts
|
|
21941
22556
|
var WeaveStrokeNode = class WeaveStrokeNode extends WeaveNode {
|
|
21942
22557
|
config;
|
|
22558
|
+
previewLook = null;
|
|
21943
22559
|
nodeType = WEAVE_STROKE_NODE_TYPE;
|
|
21944
22560
|
initialize = void 0;
|
|
21945
22561
|
/**
|
|
@@ -22012,10 +22628,11 @@ var WeaveStrokeNode = class WeaveStrokeNode extends WeaveNode {
|
|
|
22012
22628
|
* emitted as a single edge - subdividing a straight line adds vertices
|
|
22013
22629
|
* without changing a pixel.
|
|
22014
22630
|
*/
|
|
22015
|
-
getSplinePoints(pts, resolution = this.config.splineResolution) {
|
|
22631
|
+
getSplinePoints(pts, resolution = this.config.splineResolution, opts) {
|
|
22016
22632
|
if (pts.length < 2) return [...pts];
|
|
22017
|
-
const targetEdge = this.config.splineTargetEdge;
|
|
22018
|
-
const maxSteps = this.config.splineMaxSteps;
|
|
22633
|
+
const targetEdge = opts?.targetEdge ?? this.config.splineTargetEdge;
|
|
22634
|
+
const maxSteps = opts?.maxSteps ?? this.config.splineMaxSteps;
|
|
22635
|
+
const flatness = opts?.flatness ?? WeaveStrokeNode.SPLINE_FLATNESS_EPSILON;
|
|
22019
22636
|
const result = [];
|
|
22020
22637
|
for (let i = -1; i < pts.length - 2; i++) {
|
|
22021
22638
|
const p0 = pts[Math.max(i, 0)];
|
|
@@ -22032,8 +22649,12 @@ var WeaveStrokeNode = class WeaveStrokeNode extends WeaveNode {
|
|
|
22032
22649
|
const at = this.catmullRomAt(p0, p1, p2, p3, probe);
|
|
22033
22650
|
deviation = Math.max(deviation, this.distanceToSegment(at, p1, p2));
|
|
22034
22651
|
}
|
|
22035
|
-
const steps = deviation <
|
|
22036
|
-
for (let s = 0; s < steps; s++)
|
|
22652
|
+
const steps = deviation < flatness ? 1 : Math.min(Math.max(Math.ceil(chord / targetEdge), resolution), maxSteps);
|
|
22653
|
+
for (let s = 0; s < steps; s++) {
|
|
22654
|
+
const q = this.catmullRomAt(p0, p1, p2, p3, s / steps);
|
|
22655
|
+
if (opts?.pressure === "linear") q.pressure = p1.pressure + (p2.pressure - p1.pressure) * (s / steps);
|
|
22656
|
+
result.push(q);
|
|
22657
|
+
}
|
|
22037
22658
|
}
|
|
22038
22659
|
result.push(pts[pts.length - 1]);
|
|
22039
22660
|
return result;
|
|
@@ -22060,7 +22681,7 @@ var WeaveStrokeNode = class WeaveStrokeNode extends WeaveNode {
|
|
|
22060
22681
|
* result by well under a pixel (95th-percentile outline edge 1.72px vs
|
|
22061
22682
|
* 1.76px; byte-identical on long, gently-curved strokes).
|
|
22062
22683
|
*/
|
|
22063
|
-
drawDashPolygon(ctx, leftSide, rightSide, color) {
|
|
22684
|
+
drawDashPolygon(ctx, leftSide, rightSide, color, mergeCaps = false) {
|
|
22064
22685
|
const capStartL = leftSide[0];
|
|
22065
22686
|
const capStartR = rightSide[0];
|
|
22066
22687
|
const capEndL = leftSide.at(-1);
|
|
@@ -22072,6 +22693,24 @@ var WeaveStrokeNode = class WeaveStrokeNode extends WeaveNode {
|
|
|
22072
22693
|
for (const p of leftSide) ctx.lineTo(p.x, p.y);
|
|
22073
22694
|
for (const p of outlineRight) ctx.lineTo(p.x, p.y);
|
|
22074
22695
|
ctx.closePath();
|
|
22696
|
+
if (mergeCaps) {
|
|
22697
|
+
const outline = [...leftSide, ...outlineRight];
|
|
22698
|
+
let area = 0;
|
|
22699
|
+
for (let i = 0; i < outline.length; i++) {
|
|
22700
|
+
const a = outline[i];
|
|
22701
|
+
const b = outline[(i + 1) % outline.length];
|
|
22702
|
+
area += a.x * b.y - b.x * a.y;
|
|
22703
|
+
}
|
|
22704
|
+
for (const [a, b] of [[capStartL, capStartR], [capEndL, capEndR]]) {
|
|
22705
|
+
const cx = (a.x + b.x) / 2;
|
|
22706
|
+
const cy = (a.y + b.y) / 2;
|
|
22707
|
+
const r = Math.hypot(a.x - b.x, a.y - b.y) / 2;
|
|
22708
|
+
ctx.moveTo(cx + r, cy);
|
|
22709
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2, area < 0);
|
|
22710
|
+
}
|
|
22711
|
+
ctx.fill();
|
|
22712
|
+
return;
|
|
22713
|
+
}
|
|
22075
22714
|
ctx.fill();
|
|
22076
22715
|
this.drawRoundCap(ctx, capStartL, capStartR, color);
|
|
22077
22716
|
this.drawRoundCap(ctx, capEndL, capEndR, color);
|
|
@@ -22089,6 +22728,13 @@ var WeaveStrokeNode = class WeaveStrokeNode extends WeaveNode {
|
|
|
22089
22728
|
}
|
|
22090
22729
|
const filtered = this.resamplePoints(pts, this.config.resamplingSpacing);
|
|
22091
22730
|
const centerline = this.getSplinePoints(filtered, this.config.splineResolution);
|
|
22731
|
+
this.drawCenterlineRibbon(ctx, centerline, (_i, p) => Math.max(baseW * p.pressure / 2, .5), color, dash, false);
|
|
22732
|
+
}
|
|
22733
|
+
/**
|
|
22734
|
+
* Draws the offset-outline ribbon along an already prepared centerline.
|
|
22735
|
+
* `halfWidth` gives the half-width at each centerline point.
|
|
22736
|
+
*/
|
|
22737
|
+
drawCenterlineRibbon(ctx, centerline, halfWidth, color, dash, mergeCaps) {
|
|
22092
22738
|
let dashIndex = 0;
|
|
22093
22739
|
let dashOn = true;
|
|
22094
22740
|
let dashRemaining = dash.length ? dash[0] : Infinity;
|
|
@@ -22102,7 +22748,7 @@ var WeaveStrokeNode = class WeaveStrokeNode extends WeaveNode {
|
|
|
22102
22748
|
leftSide.push(pendingEndLeft);
|
|
22103
22749
|
rightSide.push(pendingEndRight);
|
|
22104
22750
|
}
|
|
22105
|
-
this.drawDashPolygon(ctx, leftSide, rightSide, color);
|
|
22751
|
+
this.drawDashPolygon(ctx, leftSide, rightSide, color, mergeCaps);
|
|
22106
22752
|
};
|
|
22107
22753
|
for (let i = 0; i < centerline.length - 1; i++) {
|
|
22108
22754
|
const p0 = centerline[i];
|
|
@@ -22112,8 +22758,8 @@ var WeaveStrokeNode = class WeaveStrokeNode extends WeaveNode {
|
|
|
22112
22758
|
const segLen = Math.hypot(dx, dy) || 1;
|
|
22113
22759
|
const nx = -dy / segLen;
|
|
22114
22760
|
const ny = dx / segLen;
|
|
22115
|
-
const w0 =
|
|
22116
|
-
const w1 =
|
|
22761
|
+
const w0 = halfWidth(i, p0);
|
|
22762
|
+
const w1 = halfWidth(i + 1, p1);
|
|
22117
22763
|
let traveled = 0;
|
|
22118
22764
|
while (traveled < segLen) {
|
|
22119
22765
|
const step = Math.min(dashRemaining, segLen - traveled);
|
|
@@ -22163,12 +22809,107 @@ var WeaveStrokeNode = class WeaveStrokeNode extends WeaveNode {
|
|
|
22163
22809
|
}
|
|
22164
22810
|
flushRun();
|
|
22165
22811
|
}
|
|
22812
|
+
/**
|
|
22813
|
+
* The look a stroke is drawn with: the viewer-side preview if one is on,
|
|
22814
|
+
* otherwise the look saved with the stroke. `null` draws the legacy ribbon.
|
|
22815
|
+
*/
|
|
22816
|
+
resolveLook(shape) {
|
|
22817
|
+
return this.previewLook ?? readStrokeLook(shape.getAttrs().look);
|
|
22818
|
+
}
|
|
22819
|
+
/**
|
|
22820
|
+
* Draws every stroke of this screen with `look`, without changing any saved
|
|
22821
|
+
* stroke and without other users seeing it. `null` turns the preview off.
|
|
22822
|
+
*/
|
|
22823
|
+
setLookPreview(look) {
|
|
22824
|
+
this.previewLook = look === null ? null : readStrokeLook(look);
|
|
22825
|
+
this.redrawStrokes();
|
|
22826
|
+
}
|
|
22827
|
+
getLookPreview() {
|
|
22828
|
+
return this.previewLook;
|
|
22829
|
+
}
|
|
22830
|
+
redrawStrokes() {
|
|
22831
|
+
const stage = this.instance?.getStage?.();
|
|
22832
|
+
if (!stage || typeof stage.find !== "function") return;
|
|
22833
|
+
const layers = /* @__PURE__ */ new Set();
|
|
22834
|
+
for (const node of stage.find(".node")) if (node.getAttrs().nodeType === "stroke") layers.add(node.getLayer());
|
|
22835
|
+
for (const layer of layers) layer?.batchDraw();
|
|
22836
|
+
}
|
|
22837
|
+
/** Screen pixels per canvas unit at a shape. */
|
|
22838
|
+
screenScale(shape) {
|
|
22839
|
+
const scale = shape.getAbsoluteScale?.().x;
|
|
22840
|
+
return typeof scale === "number" && Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
22841
|
+
}
|
|
22842
|
+
/**
|
|
22843
|
+
* The centerline of a stroke with a look: the points resampled and joined by
|
|
22844
|
+
* the look's curve. Spacing and detail are in screen pixels when the look
|
|
22845
|
+
* follows the zoom, in canvas units otherwise; `scale` is screen pixels per
|
|
22846
|
+
* canvas unit.
|
|
22847
|
+
*/
|
|
22848
|
+
buildLookCenterline(look, scale, pts) {
|
|
22849
|
+
const toCanvas = (px) => look.zoomAware ? px / scale : px;
|
|
22850
|
+
const edge = toCanvas(look.edgePx);
|
|
22851
|
+
const flatness = toCanvas(WeaveStrokeNode.SPLINE_FLATNESS_EPSILON);
|
|
22852
|
+
const maxSteps = look.zoomAware ? Math.max(this.config.splineMaxSteps, 256) : this.config.splineMaxSteps;
|
|
22853
|
+
const filtered = this.resamplePoints(pts, toCanvas(look.spacingPx));
|
|
22854
|
+
if (look.curve === "centripetal") return getCentripetalPoints(filtered, {
|
|
22855
|
+
edge,
|
|
22856
|
+
minSteps: 2,
|
|
22857
|
+
maxSteps,
|
|
22858
|
+
flatness,
|
|
22859
|
+
pressure: look.pressureInterp
|
|
22860
|
+
});
|
|
22861
|
+
if (look.curve === "none") return filtered;
|
|
22862
|
+
return this.getSplinePoints(filtered, this.config.splineResolution, {
|
|
22863
|
+
targetEdge: edge,
|
|
22864
|
+
maxSteps,
|
|
22865
|
+
flatness,
|
|
22866
|
+
pressure: look.pressureInterp
|
|
22867
|
+
});
|
|
22868
|
+
}
|
|
22869
|
+
/**
|
|
22870
|
+
* Draws a stroke that has a look: the centerline is built with the look's
|
|
22871
|
+
* curve, spacing and detail, widths follow its width rule, and the outline is
|
|
22872
|
+
* the legacy ribbon or the round one.
|
|
22873
|
+
*/
|
|
22874
|
+
drawRibbonWithLook(ctx, shape, look, pts, baseW, color, dash) {
|
|
22875
|
+
if (!pts) return;
|
|
22876
|
+
if (pts.length < 2) {
|
|
22877
|
+
const pt = pts[0];
|
|
22878
|
+
const r = halfWidthAt(look, baseW, pt.pressure, Infinity, Infinity);
|
|
22879
|
+
ctx.beginPath();
|
|
22880
|
+
ctx.fillStyle = color;
|
|
22881
|
+
ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2);
|
|
22882
|
+
ctx.fill();
|
|
22883
|
+
return;
|
|
22884
|
+
}
|
|
22885
|
+
const scale = this.screenScale(shape);
|
|
22886
|
+
const centerline = this.buildLookCenterline(look, scale, pts);
|
|
22887
|
+
const unit = look.zoomAware ? scale : 1;
|
|
22888
|
+
const along = [0];
|
|
22889
|
+
for (let i = 1; i < centerline.length; i++) along.push(along[i - 1] + Math.hypot(centerline[i].x - centerline[i - 1].x, centerline[i].y - centerline[i - 1].y) * unit);
|
|
22890
|
+
const total = along[along.length - 1];
|
|
22891
|
+
const halfWidth = (i, p) => halfWidthAt(look, baseW, p.pressure, along[i], total - along[i]);
|
|
22892
|
+
if (look.outline === "legacy") {
|
|
22893
|
+
this.drawCenterlineRibbon(ctx, centerline, halfWidth, color, dash, look.caps === "merged");
|
|
22894
|
+
return;
|
|
22895
|
+
}
|
|
22896
|
+
drawRoundOutline(ctx, centerline.map((p, i) => ({
|
|
22897
|
+
x: p.x,
|
|
22898
|
+
y: p.y,
|
|
22899
|
+
r: halfWidth(i, p)
|
|
22900
|
+
})), color, dash, scale);
|
|
22901
|
+
}
|
|
22166
22902
|
drawShape(ctx, shape) {
|
|
22167
22903
|
const strokeElements = shape.getAttrs().strokeElements;
|
|
22168
22904
|
if (strokeElements?.length === 0) return;
|
|
22169
22905
|
const color = shape.getAttrs().stroke ?? "black";
|
|
22170
22906
|
const strokeWidth = shape.getAttrs().strokeWidth ?? 1;
|
|
22171
22907
|
const dash = shape.getAttrs().dash ?? [];
|
|
22908
|
+
const look = this.resolveLook(shape);
|
|
22909
|
+
if (look) {
|
|
22910
|
+
this.drawRibbonWithLook(ctx, shape, look, strokeElements, strokeWidth, color, dash);
|
|
22911
|
+
return;
|
|
22912
|
+
}
|
|
22172
22913
|
this.drawRibbonWithDash(ctx, strokeElements, strokeWidth, color, dash);
|
|
22173
22914
|
}
|
|
22174
22915
|
onRender(props) {
|
|
@@ -28222,6 +28963,8 @@ var WeaveLineToolAction = class extends WeaveAction {
|
|
|
28222
28963
|
//#endregion
|
|
28223
28964
|
//#region src/actions/brush-tool/constants.ts
|
|
28224
28965
|
const BRUSH_TOOL_ACTION_NAME = "brushTool";
|
|
28966
|
+
/** Name under which the lab plugin registers itself. */
|
|
28967
|
+
const BRUSH_LAB_PLUGIN_NAME = "brushLab";
|
|
28225
28968
|
const BRUSH_TOOL_STATE = {
|
|
28226
28969
|
["INACTIVE"]: "inactive",
|
|
28227
28970
|
["IDLE"]: "idle",
|
|
@@ -28239,135 +28982,2284 @@ const PREVIEW_ID_SUFFIX = "--preview";
|
|
|
28239
28982
|
* Bounds the handover so a preview can never be orphaned on screen.
|
|
28240
28983
|
*/
|
|
28241
28984
|
const PREVIEW_HANDOVER_MAX_FRAMES = 30;
|
|
28985
|
+
const BRUSH_INPUT_PROFILE_KEYS = [
|
|
28986
|
+
"mouse",
|
|
28987
|
+
"touch",
|
|
28988
|
+
"pen.applePencil",
|
|
28989
|
+
"pen.desktop"
|
|
28990
|
+
];
|
|
28991
|
+
/**
|
|
28992
|
+
* The legacy input profile: reproduces, point for point, what the brush tool
|
|
28993
|
+
* recorded before profiles existed. Every profile starts from it.
|
|
28994
|
+
*/
|
|
28995
|
+
const BRUSH_LEGACY_INPUT_PROFILE = {
|
|
28996
|
+
capture: {
|
|
28997
|
+
source: "legacy",
|
|
28998
|
+
clock: "now",
|
|
28999
|
+
minMovePx: 0,
|
|
29000
|
+
predicted: "tail",
|
|
29001
|
+
event: "pointermove"
|
|
29002
|
+
},
|
|
29003
|
+
pressure: {
|
|
29004
|
+
zero: "legacy",
|
|
29005
|
+
filter: "legacy",
|
|
29006
|
+
timeConstantMs: 40,
|
|
29007
|
+
oneEuro: {
|
|
29008
|
+
minCutoff: 1,
|
|
29009
|
+
beta: .01
|
|
29010
|
+
},
|
|
29011
|
+
floor: .15,
|
|
29012
|
+
start: "backfill",
|
|
29013
|
+
startSamples: 3,
|
|
29014
|
+
end: "backfill"
|
|
29015
|
+
},
|
|
29016
|
+
stabilizer: {
|
|
29017
|
+
type: "none",
|
|
29018
|
+
minCutoff: 3,
|
|
29019
|
+
beta: .01,
|
|
29020
|
+
radiusPx: 4,
|
|
29021
|
+
amount: .3,
|
|
29022
|
+
zoomAware: false,
|
|
29023
|
+
catchUpOnEnd: true
|
|
29024
|
+
},
|
|
29025
|
+
finalize: {
|
|
29026
|
+
mode: "dp",
|
|
29027
|
+
toleranceUnit: "canvas",
|
|
29028
|
+
pressureWeight: 20,
|
|
29029
|
+
resampleSpacingPx: 2
|
|
29030
|
+
},
|
|
29031
|
+
touchGuard: {
|
|
29032
|
+
waitMs: 0,
|
|
29033
|
+
movePx: 0,
|
|
29034
|
+
cancelOnSecondTouch: false,
|
|
29035
|
+
ignoreAfterPen: false
|
|
29036
|
+
}
|
|
29037
|
+
};
|
|
28242
29038
|
const BRUSH_TOOL_DEFAULT_CONFIG = {
|
|
29039
|
+
input: {
|
|
29040
|
+
forceProfile: null,
|
|
29041
|
+
profiles: {
|
|
29042
|
+
mouse: BRUSH_LEGACY_INPUT_PROFILE,
|
|
29043
|
+
touch: BRUSH_LEGACY_INPUT_PROFILE,
|
|
29044
|
+
"pen.applePencil": BRUSH_LEGACY_INPUT_PROFILE,
|
|
29045
|
+
"pen.desktop": BRUSH_LEGACY_INPUT_PROFILE
|
|
29046
|
+
}
|
|
29047
|
+
},
|
|
28243
29048
|
interpolationSteps: 10,
|
|
28244
29049
|
simplifyTolerance: .5
|
|
28245
29050
|
};
|
|
28246
29051
|
//#endregion
|
|
28247
|
-
//#region src/actions/brush-tool/
|
|
28248
|
-
|
|
28249
|
-
|
|
28250
|
-
|
|
28251
|
-
|
|
28252
|
-
|
|
28253
|
-
|
|
28254
|
-
|
|
28255
|
-
|
|
28256
|
-
|
|
28257
|
-
|
|
28258
|
-
|
|
28259
|
-
|
|
28260
|
-
|
|
28261
|
-
lastSmoothedPressure = .5;
|
|
28262
|
-
hasPressureSample = false;
|
|
28263
|
-
lastPointerPos = null;
|
|
28264
|
-
lastPointerTime = 0;
|
|
28265
|
-
predictedCount = 0;
|
|
28266
|
-
onPropsChange = void 0;
|
|
28267
|
-
onInit = void 0;
|
|
28268
|
-
constructor(params) {
|
|
28269
|
-
super();
|
|
28270
|
-
this.config = mergeExceptArrays(BRUSH_TOOL_DEFAULT_CONFIG, params?.config ?? {});
|
|
28271
|
-
this.initialize();
|
|
29052
|
+
//#region src/actions/brush-tool/stages/stabilizer.ts
|
|
29053
|
+
/** Time assumed between two samples whose times do not advance, in ms. */
|
|
29054
|
+
const NOMINAL_SAMPLE_INTERVAL_MS$1 = 5;
|
|
29055
|
+
const seconds = (now, last) => (now - last > 0 ? now - last : NOMINAL_SAMPLE_INTERVAL_MS$1) / 1e3;
|
|
29056
|
+
/**
|
|
29057
|
+
* Scale between canvas units and the units distances are configured in: screen
|
|
29058
|
+
* pixels when the stabilizer is zoom-aware, canvas units otherwise.
|
|
29059
|
+
*/
|
|
29060
|
+
const unitScale = (cfg, ctx) => cfg.zoomAware && ctx.zoom > 0 && Number.isFinite(ctx.zoom) ? ctx.zoom : 1;
|
|
29061
|
+
var NoStabilizer = class {
|
|
29062
|
+
reset() {}
|
|
29063
|
+
seed() {}
|
|
29064
|
+
apply(point) {
|
|
29065
|
+
return point;
|
|
28272
29066
|
}
|
|
28273
|
-
|
|
28274
|
-
|
|
28275
|
-
|
|
28276
|
-
|
|
28277
|
-
|
|
28278
|
-
|
|
28279
|
-
|
|
28280
|
-
|
|
28281
|
-
this.
|
|
28282
|
-
this.
|
|
29067
|
+
};
|
|
29068
|
+
/** One Euro low-pass filter on one axis. */
|
|
29069
|
+
var OneEuroAxis = class {
|
|
29070
|
+
last = 0;
|
|
29071
|
+
derivative = 0;
|
|
29072
|
+
lastTime = 0;
|
|
29073
|
+
seeded = false;
|
|
29074
|
+
reset() {
|
|
29075
|
+
this.seeded = false;
|
|
29076
|
+
this.derivative = 0;
|
|
29077
|
+
}
|
|
29078
|
+
seed(value, time) {
|
|
29079
|
+
this.last = value;
|
|
29080
|
+
this.lastTime = time;
|
|
29081
|
+
this.derivative = 0;
|
|
29082
|
+
this.seeded = true;
|
|
29083
|
+
}
|
|
29084
|
+
filter(value, time, minCutoff, beta) {
|
|
29085
|
+
if (!this.seeded) {
|
|
29086
|
+
this.seed(value, time);
|
|
29087
|
+
return value;
|
|
29088
|
+
}
|
|
29089
|
+
const dt = seconds(time, this.lastTime);
|
|
29090
|
+
const factor = (cutoff) => {
|
|
29091
|
+
const r = 2 * Math.PI * cutoff * dt;
|
|
29092
|
+
return r / (r + 1);
|
|
29093
|
+
};
|
|
29094
|
+
const a = factor(1);
|
|
29095
|
+
this.derivative = a * ((value - this.last) / dt) + (1 - a) * this.derivative;
|
|
29096
|
+
const alpha = factor(minCutoff + beta * Math.abs(this.derivative));
|
|
29097
|
+
const filtered = alpha * value + (1 - alpha) * this.last;
|
|
29098
|
+
this.last = Number.isFinite(filtered) ? filtered : value;
|
|
29099
|
+
this.lastTime = time;
|
|
29100
|
+
return this.last;
|
|
28283
29101
|
}
|
|
28284
|
-
|
|
28285
|
-
|
|
29102
|
+
};
|
|
29103
|
+
/**
|
|
29104
|
+
* One Euro filter (Casiez, Roussel, Vogel 2012) on the pen position: slow
|
|
29105
|
+
* movement is smoothed, fast movement is followed closely.
|
|
29106
|
+
*/
|
|
29107
|
+
var OneEuroStabilizer = class {
|
|
29108
|
+
cfg;
|
|
29109
|
+
x = new OneEuroAxis();
|
|
29110
|
+
y = new OneEuroAxis();
|
|
29111
|
+
constructor(cfg) {
|
|
29112
|
+
this.cfg = cfg;
|
|
28286
29113
|
}
|
|
28287
|
-
|
|
28288
|
-
|
|
28289
|
-
|
|
28290
|
-
strokeWidth: 1,
|
|
28291
|
-
opacity: 1
|
|
28292
|
-
};
|
|
29114
|
+
reset() {
|
|
29115
|
+
this.x.reset();
|
|
29116
|
+
this.y.reset();
|
|
28293
29117
|
}
|
|
28294
|
-
|
|
28295
|
-
|
|
28296
|
-
|
|
28297
|
-
|
|
28298
|
-
|
|
28299
|
-
|
|
28300
|
-
|
|
28301
|
-
|
|
28302
|
-
|
|
28303
|
-
|
|
28304
|
-
* Timing intentionally uses `performance.now()` (not the event's own
|
|
28305
|
-
* `timeStamp`), matching the original behaviour: the two are not
|
|
28306
|
-
* guaranteed to share a time origin, and mixing them corrupts the
|
|
28307
|
-
* velocity term that drives `alpha`.
|
|
28308
|
-
*
|
|
28309
|
-
* The first sample of a stroke SEEDS the filter with its own value
|
|
28310
|
-
* rather than being blended toward the neutral 0.5 baseline. Blending
|
|
28311
|
-
* from a constant gives every stroke a warm-up transient: the filter
|
|
28312
|
-
* starts at 0.5 and converges toward the pressure actually being
|
|
28313
|
-
* applied, so a stroke drawn lighter than 0.5 renders visibly fatter at
|
|
28314
|
-
* its start (and heavier than 0.5, thinner) until the filter catches
|
|
28315
|
-
* up. Seeding removes that transient entirely - smoothing then only
|
|
28316
|
-
* ever acts between real samples, which is its actual purpose.
|
|
28317
|
-
*/
|
|
28318
|
-
smoothPressureSample(raw, clientX, clientY) {
|
|
28319
|
-
const now = performance.now();
|
|
28320
|
-
let velocity = 0;
|
|
28321
|
-
const hasPosition = Number.isFinite(clientX) && Number.isFinite(clientY);
|
|
28322
|
-
if (hasPosition && this.lastPointerPos && now - this.lastPointerTime > 0) {
|
|
28323
|
-
const dx = clientX - this.lastPointerPos.x;
|
|
28324
|
-
const dy = clientY - this.lastPointerPos.y;
|
|
28325
|
-
velocity = Math.hypot(dx, dy) / (now - this.lastPointerTime) * 1e3;
|
|
28326
|
-
}
|
|
28327
|
-
if (hasPosition) this.lastPointerPos = {
|
|
28328
|
-
x: clientX,
|
|
28329
|
-
y: clientY
|
|
29118
|
+
seed(point, ctx) {
|
|
29119
|
+
const s = unitScale(this.cfg, ctx);
|
|
29120
|
+
this.x.seed(point.x * s, ctx.now);
|
|
29121
|
+
this.y.seed(point.y * s, ctx.now);
|
|
29122
|
+
}
|
|
29123
|
+
apply(point, ctx) {
|
|
29124
|
+
const s = unitScale(this.cfg, ctx);
|
|
29125
|
+
return {
|
|
29126
|
+
x: this.x.filter(point.x * s, ctx.now, this.cfg.minCutoff, this.cfg.beta) / s,
|
|
29127
|
+
y: this.y.filter(point.y * s, ctx.now, this.cfg.minCutoff, this.cfg.beta) / s
|
|
28330
29128
|
};
|
|
28331
|
-
this.lastPointerTime = now;
|
|
28332
|
-
const safeRaw = Number.isFinite(raw) ? raw : .5;
|
|
28333
|
-
if (!this.hasPressureSample) {
|
|
28334
|
-
this.hasPressureSample = true;
|
|
28335
|
-
this.lastSmoothedPressure = safeRaw;
|
|
28336
|
-
return Math.max(this.lastSmoothedPressure, .15);
|
|
28337
|
-
}
|
|
28338
|
-
const alpha = Math.min(Math.max(velocity / 1500, .15), .6);
|
|
28339
|
-
const smoothed = alpha * safeRaw + (1 - alpha) * this.lastSmoothedPressure;
|
|
28340
|
-
this.lastSmoothedPressure = Number.isFinite(smoothed) ? smoothed : safeRaw;
|
|
28341
|
-
return Math.max(this.lastSmoothedPressure, .15);
|
|
28342
29129
|
}
|
|
28343
|
-
|
|
29130
|
+
};
|
|
29131
|
+
/**
|
|
29132
|
+
* Pulled string: the recorded point is held back on a leash of fixed length
|
|
29133
|
+
* and only moves when the pen pulls it taut, so movement inside the leash
|
|
29134
|
+
* leaves no mark and tight curves are rounded.
|
|
29135
|
+
*/
|
|
29136
|
+
var PulledStringStabilizer = class {
|
|
29137
|
+
cfg;
|
|
29138
|
+
tip = null;
|
|
29139
|
+
constructor(cfg) {
|
|
29140
|
+
this.cfg = cfg;
|
|
29141
|
+
}
|
|
29142
|
+
reset() {
|
|
29143
|
+
this.tip = null;
|
|
29144
|
+
}
|
|
29145
|
+
seed(point) {
|
|
29146
|
+
this.tip = { ...point };
|
|
29147
|
+
}
|
|
29148
|
+
apply(point, ctx) {
|
|
29149
|
+
if (!this.tip) {
|
|
29150
|
+
this.tip = { ...point };
|
|
29151
|
+
return point;
|
|
29152
|
+
}
|
|
29153
|
+
const leash = this.cfg.radiusPx / unitScale(this.cfg, ctx);
|
|
29154
|
+
const dx = point.x - this.tip.x;
|
|
29155
|
+
const dy = point.y - this.tip.y;
|
|
29156
|
+
const distance = Math.hypot(dx, dy);
|
|
29157
|
+
if (distance > leash && distance > 0) {
|
|
29158
|
+
const pull = (distance - leash) / distance;
|
|
29159
|
+
this.tip = {
|
|
29160
|
+
x: this.tip.x + dx * pull,
|
|
29161
|
+
y: this.tip.y + dy * pull
|
|
29162
|
+
};
|
|
29163
|
+
}
|
|
29164
|
+
return { ...this.tip };
|
|
29165
|
+
}
|
|
29166
|
+
};
|
|
29167
|
+
/** Longest time constant of the streamline stabilizer, at amount 1, in ms. */
|
|
29168
|
+
const STREAMLINE_MAX_TAU_MS = 80;
|
|
29169
|
+
/**
|
|
29170
|
+
* Streamline: the recorded point eases towards the pen with a time constant
|
|
29171
|
+
* proportional to `amount`, so the result does not depend on how often samples
|
|
29172
|
+
* arrive. Amount 0 follows the pen exactly.
|
|
29173
|
+
*/
|
|
29174
|
+
var StreamlineStabilizer = class {
|
|
29175
|
+
cfg;
|
|
29176
|
+
tip = null;
|
|
29177
|
+
lastTime = 0;
|
|
29178
|
+
constructor(cfg) {
|
|
29179
|
+
this.cfg = cfg;
|
|
29180
|
+
}
|
|
29181
|
+
reset() {
|
|
29182
|
+
this.tip = null;
|
|
29183
|
+
}
|
|
29184
|
+
seed(point, ctx) {
|
|
29185
|
+
this.tip = { ...point };
|
|
29186
|
+
this.lastTime = ctx.now;
|
|
29187
|
+
}
|
|
29188
|
+
apply(point, ctx) {
|
|
29189
|
+
const tau = this.cfg.amount * STREAMLINE_MAX_TAU_MS;
|
|
29190
|
+
if (!this.tip || tau <= 0) {
|
|
29191
|
+
this.tip = { ...point };
|
|
29192
|
+
this.lastTime = ctx.now;
|
|
29193
|
+
return point;
|
|
29194
|
+
}
|
|
29195
|
+
const dt = seconds(ctx.now, this.lastTime) * 1e3;
|
|
29196
|
+
const alpha = 1 - Math.exp(-dt / tau);
|
|
29197
|
+
this.tip = {
|
|
29198
|
+
x: this.tip.x + (point.x - this.tip.x) * alpha,
|
|
29199
|
+
y: this.tip.y + (point.y - this.tip.y) * alpha
|
|
29200
|
+
};
|
|
29201
|
+
this.lastTime = ctx.now;
|
|
29202
|
+
return { ...this.tip };
|
|
29203
|
+
}
|
|
29204
|
+
};
|
|
29205
|
+
/** Creates the stabilizer selected by a profile. */
|
|
29206
|
+
function createStabilizer(cfg) {
|
|
29207
|
+
switch (cfg.type) {
|
|
29208
|
+
case "oneEuro": return new OneEuroStabilizer(cfg);
|
|
29209
|
+
case "pulledString": return new PulledStringStabilizer(cfg);
|
|
29210
|
+
case "streamline": return new StreamlineStabilizer(cfg);
|
|
29211
|
+
default: return new NoStabilizer();
|
|
29212
|
+
}
|
|
29213
|
+
}
|
|
29214
|
+
//#endregion
|
|
29215
|
+
//#region src/actions/brush-tool/stages/pressure.ts
|
|
29216
|
+
/** Time assumed between two samples whose times do not advance, in ms. */
|
|
29217
|
+
const NOMINAL_SAMPLE_INTERVAL_MS = 5;
|
|
29218
|
+
function resetPressureState(state) {
|
|
29219
|
+
state.lastSmoothedPressure = .5;
|
|
29220
|
+
state.hasPressureSample = false;
|
|
29221
|
+
state.lastPointerPos = null;
|
|
29222
|
+
state.lastPointerTime = 0;
|
|
29223
|
+
state.predictedCount = 0;
|
|
29224
|
+
state.lastDerivative = 0;
|
|
29225
|
+
state.lastNonZeroPressure = null;
|
|
29226
|
+
}
|
|
29227
|
+
/**
|
|
29228
|
+
* Raw pressure a pointer sample reports: the device's own reading for a pen, a
|
|
29229
|
+
* neutral constant for devices that do not sense pressure.
|
|
29230
|
+
*
|
|
29231
|
+
* `zero` decides what a reported pressure of 0 means for a pen: `legacy`
|
|
29232
|
+
* replaces it with 0.5, `keep` records it (the pressure floor still applies),
|
|
29233
|
+
* `holdLast` repeats the last non-zero reading. `commit: false` reads without
|
|
29234
|
+
* remembering the value, for speculative samples.
|
|
29235
|
+
*/
|
|
29236
|
+
function getRawPressure(pointerType, pressure, zero = "legacy", state, commit = true) {
|
|
29237
|
+
if (pointerType !== "pen") return .5;
|
|
29238
|
+
const value = typeof pressure === "number" && Number.isFinite(pressure) ? pressure : 0;
|
|
29239
|
+
switch (zero) {
|
|
29240
|
+
case "keep": return typeof pressure === "number" && Number.isFinite(pressure) ? pressure : .5;
|
|
29241
|
+
case "holdLast":
|
|
29242
|
+
if (value > 0) {
|
|
29243
|
+
if (commit && state) state.lastNonZeroPressure = value;
|
|
29244
|
+
return value;
|
|
29245
|
+
}
|
|
29246
|
+
return state?.lastNonZeroPressure ?? .5;
|
|
29247
|
+
default: return value || .5;
|
|
29248
|
+
}
|
|
29249
|
+
}
|
|
29250
|
+
/**
|
|
29251
|
+
* Legacy velocity-adaptive exponential smoothing of one raw pressure sample.
|
|
29252
|
+
*
|
|
29253
|
+
* The first sample of a stroke seeds the filter with its own value. Fast
|
|
29254
|
+
* movement gives a higher alpha (more responsive), slow movement a lower one.
|
|
29255
|
+
*/
|
|
29256
|
+
function smoothLegacy(state, cfg, raw, clientX, clientY, now) {
|
|
29257
|
+
let velocity = 0;
|
|
29258
|
+
const hasPosition = Number.isFinite(clientX) && Number.isFinite(clientY);
|
|
29259
|
+
if (hasPosition && state.lastPointerPos && now - state.lastPointerTime > 0) {
|
|
29260
|
+
const dx = clientX - state.lastPointerPos.x;
|
|
29261
|
+
const dy = clientY - state.lastPointerPos.y;
|
|
29262
|
+
velocity = Math.hypot(dx, dy) / (now - state.lastPointerTime) * 1e3;
|
|
29263
|
+
}
|
|
29264
|
+
if (hasPosition) state.lastPointerPos = {
|
|
29265
|
+
x: clientX,
|
|
29266
|
+
y: clientY
|
|
29267
|
+
};
|
|
29268
|
+
state.lastPointerTime = now;
|
|
29269
|
+
const safeRaw = Number.isFinite(raw) ? raw : .5;
|
|
29270
|
+
if (!state.hasPressureSample) {
|
|
29271
|
+
state.hasPressureSample = true;
|
|
29272
|
+
state.lastSmoothedPressure = safeRaw;
|
|
29273
|
+
return Math.max(state.lastSmoothedPressure, cfg.floor);
|
|
29274
|
+
}
|
|
29275
|
+
const alpha = Math.min(Math.max(velocity / 1500, .15), .6);
|
|
29276
|
+
const smoothed = alpha * safeRaw + (1 - alpha) * state.lastSmoothedPressure;
|
|
29277
|
+
state.lastSmoothedPressure = Number.isFinite(smoothed) ? smoothed : safeRaw;
|
|
29278
|
+
return Math.max(state.lastSmoothedPressure, cfg.floor);
|
|
29279
|
+
}
|
|
29280
|
+
/** Seconds between two samples; a non-advancing clock assumes a nominal rate. */
|
|
29281
|
+
function elapsedSeconds(state, now) {
|
|
29282
|
+
const dt = now - state.lastPointerTime;
|
|
29283
|
+
return (dt > 0 ? dt : NOMINAL_SAMPLE_INTERVAL_MS) / 1e3;
|
|
29284
|
+
}
|
|
29285
|
+
/** Seeds a filter with its first sample. */
|
|
29286
|
+
function seed(state, value, now) {
|
|
29287
|
+
state.hasPressureSample = true;
|
|
29288
|
+
state.lastSmoothedPressure = value;
|
|
29289
|
+
state.lastPointerTime = now;
|
|
29290
|
+
state.lastDerivative = 0;
|
|
29291
|
+
}
|
|
29292
|
+
/** Exponential smoothing with a time constant, independent of the report rate. */
|
|
29293
|
+
function smoothTimeEma(state, cfg, safeRaw, now) {
|
|
29294
|
+
if (!state.hasPressureSample) {
|
|
29295
|
+
seed(state, safeRaw, now);
|
|
29296
|
+
return Math.max(safeRaw, cfg.floor);
|
|
29297
|
+
}
|
|
29298
|
+
const dt = elapsedSeconds(state, now) * 1e3;
|
|
29299
|
+
const alpha = 1 - Math.exp(-dt / Math.max(cfg.timeConstantMs, 1e-6));
|
|
29300
|
+
const smoothed = alpha * safeRaw + (1 - alpha) * state.lastSmoothedPressure;
|
|
29301
|
+
state.lastSmoothedPressure = Number.isFinite(smoothed) ? smoothed : safeRaw;
|
|
29302
|
+
state.lastPointerTime = now;
|
|
29303
|
+
return Math.max(state.lastSmoothedPressure, cfg.floor);
|
|
29304
|
+
}
|
|
29305
|
+
const smoothingFactor = (dt, cutoff) => {
|
|
29306
|
+
const r = 2 * Math.PI * cutoff * dt;
|
|
29307
|
+
return r / (r + 1);
|
|
29308
|
+
};
|
|
29309
|
+
/**
|
|
29310
|
+
* One Euro filter (Casiez, Roussel, Vogel 2012): a low-pass filter whose cutoff
|
|
29311
|
+
* rises with speed, so slow changes are smoothed and fast ones follow closely.
|
|
29312
|
+
*/
|
|
29313
|
+
function smoothOneEuro(state, cfg, safeRaw, now) {
|
|
29314
|
+
if (!state.hasPressureSample) {
|
|
29315
|
+
seed(state, safeRaw, now);
|
|
29316
|
+
return Math.max(safeRaw, cfg.floor);
|
|
29317
|
+
}
|
|
29318
|
+
const dt = elapsedSeconds(state, now);
|
|
29319
|
+
const derivative = (safeRaw - state.lastSmoothedPressure) / dt;
|
|
29320
|
+
const a = smoothingFactor(dt, 1);
|
|
29321
|
+
state.lastDerivative = a * derivative + (1 - a) * state.lastDerivative;
|
|
29322
|
+
const cutoff = cfg.oneEuro.minCutoff + cfg.oneEuro.beta * Math.abs(state.lastDerivative);
|
|
29323
|
+
const alpha = smoothingFactor(dt, cutoff);
|
|
29324
|
+
const smoothed = alpha * safeRaw + (1 - alpha) * state.lastSmoothedPressure;
|
|
29325
|
+
state.lastSmoothedPressure = Number.isFinite(smoothed) ? smoothed : safeRaw;
|
|
29326
|
+
state.lastPointerTime = now;
|
|
29327
|
+
return Math.max(state.lastSmoothedPressure, cfg.floor);
|
|
29328
|
+
}
|
|
29329
|
+
/** Smooths one raw pressure sample with the filter selected by the profile. */
|
|
29330
|
+
function smoothPressure(state, cfg, raw, clientX, clientY, now) {
|
|
29331
|
+
const safeRaw = Number.isFinite(raw) ? raw : .5;
|
|
29332
|
+
switch (cfg.filter) {
|
|
29333
|
+
case "timeEma": return smoothTimeEma(state, cfg, safeRaw, now);
|
|
29334
|
+
case "oneEuro": return smoothOneEuro(state, cfg, safeRaw, now);
|
|
29335
|
+
case "none":
|
|
29336
|
+
state.hasPressureSample = true;
|
|
29337
|
+
state.lastSmoothedPressure = safeRaw;
|
|
29338
|
+
state.lastPointerTime = now;
|
|
29339
|
+
return Math.max(safeRaw, cfg.floor);
|
|
29340
|
+
default: return smoothLegacy(state, cfg, raw, clientX, clientY, now);
|
|
29341
|
+
}
|
|
29342
|
+
}
|
|
29343
|
+
/**
|
|
29344
|
+
* Pressure of a predicted (speculative) sample. It blends against the current
|
|
29345
|
+
* state read-only, because predicted samples are rolled back and must not
|
|
29346
|
+
* advance the state real samples depend on.
|
|
29347
|
+
*/
|
|
29348
|
+
function peekPredictedPressure(state, cfg, raw) {
|
|
29349
|
+
return Math.max(.15 * raw + .85 * state.lastSmoothedPressure, cfg.floor);
|
|
29350
|
+
}
|
|
29351
|
+
/**
|
|
29352
|
+
* Start policy for the first point's pressure. A stroke's pointer-down reading
|
|
29353
|
+
* is frequently an unreliable contact spike.
|
|
29354
|
+
*
|
|
29355
|
+
* - `backfill`: the first real movement sample replaces it once it exists.
|
|
29356
|
+
* - `average`: it becomes the mean of the first `startSamples` real movement
|
|
29357
|
+
* readings, refined as each one arrives, so it never keeps the contact value.
|
|
29358
|
+
* - `none`: it is left as reported.
|
|
29359
|
+
*
|
|
29360
|
+
* Predicted samples never change it.
|
|
29361
|
+
*/
|
|
29362
|
+
function applyStartPolicy(elements, incoming, isPredicted, cfg) {
|
|
29363
|
+
if (isPredicted || elements.length === 0) return;
|
|
29364
|
+
if (cfg.start === "backfill" && elements.length === 1) elements[0] = {
|
|
29365
|
+
...elements[0],
|
|
29366
|
+
pressure: incoming.pressure
|
|
29367
|
+
};
|
|
29368
|
+
else if (cfg.start === "average" && elements.length <= cfg.startSamples) {
|
|
29369
|
+
const readings = [...elements.slice(1), incoming];
|
|
29370
|
+
elements[0] = {
|
|
29371
|
+
...elements[0],
|
|
29372
|
+
pressure: readings.reduce((sum, p) => sum + p.pressure, 0) / readings.length
|
|
29373
|
+
};
|
|
29374
|
+
}
|
|
29375
|
+
}
|
|
29376
|
+
/**
|
|
29377
|
+
* End policy. A lift-off reading can be as unreliable as the contact spike;
|
|
29378
|
+
* under `backfill` the last point takes the second-to-last point's pressure.
|
|
29379
|
+
*/
|
|
29380
|
+
function applyEndPolicy(elements, cfg) {
|
|
29381
|
+
if (cfg.end === "backfill" && elements.length >= 2) {
|
|
29382
|
+
const result = [...elements];
|
|
29383
|
+
const lastIndex = result.length - 1;
|
|
29384
|
+
result[lastIndex] = {
|
|
29385
|
+
...result[lastIndex],
|
|
29386
|
+
pressure: result[lastIndex - 1].pressure
|
|
29387
|
+
};
|
|
29388
|
+
return result;
|
|
29389
|
+
}
|
|
29390
|
+
return elements;
|
|
29391
|
+
}
|
|
29392
|
+
//#endregion
|
|
29393
|
+
//#region src/actions/brush-tool/stages/capture.ts
|
|
29394
|
+
const createCaptureState = () => ({ lastClient: null });
|
|
29395
|
+
/**
|
|
29396
|
+
* Legacy capture of a pointer move that carries batched (coalesced) samples.
|
|
29397
|
+
*
|
|
29398
|
+
* Every real sample is smoothed through the same path as any other sample but
|
|
29399
|
+
* deliberately carries no `sourceEvent`: coalesced samples keep using the
|
|
29400
|
+
* event's already-resolved pointer position. The last predicted sample, if
|
|
29401
|
+
* any, is appended as a provisional point.
|
|
29402
|
+
*
|
|
29403
|
+
* Returns `null` when the event has at most one sample, so the caller takes
|
|
29404
|
+
* the single-event path.
|
|
29405
|
+
*/
|
|
29406
|
+
function captureLegacySamples(evt, deps) {
|
|
29407
|
+
const coalescedEvents = evt.getCoalescedEvents ? evt.getCoalescedEvents() : [];
|
|
29408
|
+
if (coalescedEvents.length <= 1) return null;
|
|
29409
|
+
const samples = coalescedEvents.map((ce) => ({
|
|
29410
|
+
pressure: deps.smooth(deps.rawPressure(ce.pointerType, ce.pressure), ce.clientX, ce.clientY, ce.timeStamp),
|
|
29411
|
+
isPredicted: false
|
|
29412
|
+
}));
|
|
29413
|
+
const predictedEvents = evt.getPredictedEvents ? evt.getPredictedEvents() : [];
|
|
29414
|
+
if (predictedEvents.length > 0) {
|
|
29415
|
+
const last = predictedEvents[predictedEvents.length - 1];
|
|
29416
|
+
samples.push({
|
|
29417
|
+
pressure: deps.predictedPressure(deps.rawPressure(last.pointerType, last.pressure, false)),
|
|
29418
|
+
sourceEvent: last,
|
|
29419
|
+
isPredicted: true
|
|
29420
|
+
});
|
|
29421
|
+
}
|
|
29422
|
+
return samples;
|
|
29423
|
+
}
|
|
29424
|
+
/**
|
|
29425
|
+
* Captures every sample a pointer move carries: each batched (coalesced) sample
|
|
29426
|
+
* becomes a point at its own position, pressure and time, in the order the
|
|
29427
|
+
* browser reported them. An event without a batch is one sample.
|
|
29428
|
+
*
|
|
29429
|
+
* Positions are computed from each sample's client coordinates, so no shared
|
|
29430
|
+
* stage state is changed. Samples closer than `minMovePx` (screen px) to the
|
|
29431
|
+
* last recorded one are skipped. The browser's predicted tail is appended as a
|
|
29432
|
+
* provisional point when enabled and at least one real sample was kept.
|
|
29433
|
+
*/
|
|
29434
|
+
function captureCoalescedSamples(evt, deps, cfg, state) {
|
|
29435
|
+
const batch = evt.getCoalescedEvents ? evt.getCoalescedEvents() : [];
|
|
29436
|
+
const source = batch.length > 0 ? batch : [evt];
|
|
29437
|
+
const samples = [];
|
|
29438
|
+
for (const ce of source) {
|
|
29439
|
+
if (cfg.minMovePx > 0 && state.lastClient && Math.hypot(ce.clientX - state.lastClient.x, ce.clientY - state.lastClient.y) < cfg.minMovePx) continue;
|
|
29440
|
+
state.lastClient = {
|
|
29441
|
+
x: ce.clientX,
|
|
29442
|
+
y: ce.clientY
|
|
29443
|
+
};
|
|
29444
|
+
samples.push({
|
|
29445
|
+
pressure: deps.smooth(deps.rawPressure(ce.pointerType, ce.pressure), ce.clientX, ce.clientY, ce.timeStamp),
|
|
29446
|
+
point: deps.toContainerPoint(ce.clientX, ce.clientY),
|
|
29447
|
+
time: ce.timeStamp,
|
|
29448
|
+
isPredicted: false
|
|
29449
|
+
});
|
|
29450
|
+
}
|
|
29451
|
+
if (samples.length > 0 && cfg.predicted === "tail") {
|
|
29452
|
+
const predictedEvents = evt.getPredictedEvents ? evt.getPredictedEvents() : [];
|
|
29453
|
+
if (predictedEvents.length > 0) {
|
|
29454
|
+
const last = predictedEvents[predictedEvents.length - 1];
|
|
29455
|
+
samples.push({
|
|
29456
|
+
pressure: deps.predictedPressure(deps.rawPressure(last.pointerType, last.pressure, false)),
|
|
29457
|
+
point: deps.toContainerPoint(last.clientX, last.clientY),
|
|
29458
|
+
time: last.timeStamp,
|
|
29459
|
+
isPredicted: true
|
|
29460
|
+
});
|
|
29461
|
+
}
|
|
29462
|
+
}
|
|
29463
|
+
return samples;
|
|
29464
|
+
}
|
|
29465
|
+
//#endregion
|
|
29466
|
+
//#region src/actions/brush-tool/stages/bezier-fit.ts
|
|
29467
|
+
const sub = (a, b) => ({
|
|
29468
|
+
x: a.x - b.x,
|
|
29469
|
+
y: a.y - b.y
|
|
29470
|
+
});
|
|
29471
|
+
const add = (a, b) => ({
|
|
29472
|
+
x: a.x + b.x,
|
|
29473
|
+
y: a.y + b.y
|
|
29474
|
+
});
|
|
29475
|
+
const scale = (a, s) => ({
|
|
29476
|
+
x: a.x * s,
|
|
29477
|
+
y: a.y * s
|
|
29478
|
+
});
|
|
29479
|
+
const dot = (a, b) => a.x * b.x + a.y * b.y;
|
|
29480
|
+
const length = (a) => Math.hypot(a.x, a.y);
|
|
29481
|
+
const normalize = (a) => {
|
|
29482
|
+
const l = length(a);
|
|
29483
|
+
return l > 0 ? {
|
|
29484
|
+
x: a.x / l,
|
|
29485
|
+
y: a.y / l
|
|
29486
|
+
} : {
|
|
29487
|
+
x: 0,
|
|
29488
|
+
y: 0
|
|
29489
|
+
};
|
|
29490
|
+
};
|
|
29491
|
+
const B0 = (u) => (1 - u) ** 3;
|
|
29492
|
+
const B1 = (u) => 3 * u * (1 - u) ** 2;
|
|
29493
|
+
const B2 = (u) => 3 * u * u * (1 - u);
|
|
29494
|
+
const B3 = (u) => u ** 3;
|
|
29495
|
+
/** Point on a cubic Bézier curve. */
|
|
29496
|
+
function bezierPoint(b, t) {
|
|
29497
|
+
return {
|
|
29498
|
+
x: B0(t) * b[0].x + B1(t) * b[1].x + B2(t) * b[2].x + B3(t) * b[3].x,
|
|
29499
|
+
y: B0(t) * b[0].y + B1(t) * b[1].y + B2(t) * b[2].y + B3(t) * b[3].y
|
|
29500
|
+
};
|
|
29501
|
+
}
|
|
29502
|
+
function derivative(b, t) {
|
|
29503
|
+
const q = [
|
|
29504
|
+
sub(b[1], b[0]),
|
|
29505
|
+
sub(b[2], b[1]),
|
|
29506
|
+
sub(b[3], b[2])
|
|
29507
|
+
].map((v) => scale(v, 3));
|
|
29508
|
+
const s = 1 - t;
|
|
29509
|
+
return {
|
|
29510
|
+
x: s * s * q[0].x + 2 * s * t * q[1].x + t * t * q[2].x,
|
|
29511
|
+
y: s * s * q[0].y + 2 * s * t * q[1].y + t * t * q[2].y
|
|
29512
|
+
};
|
|
29513
|
+
}
|
|
29514
|
+
function secondDerivative(b, t) {
|
|
29515
|
+
const q = [add(sub(b[2], scale(b[1], 2)), b[0]), add(sub(b[3], scale(b[2], 2)), b[1])].map((v) => scale(v, 6));
|
|
29516
|
+
return {
|
|
29517
|
+
x: (1 - t) * q[0].x + t * q[1].x,
|
|
29518
|
+
y: (1 - t) * q[0].y + t * q[1].y
|
|
29519
|
+
};
|
|
29520
|
+
}
|
|
29521
|
+
/**
|
|
29522
|
+
* Direction of the curve at `index`, estimated over a window at least
|
|
29523
|
+
* `window` long on each side (but inside `first..last`), so that noise in the
|
|
29524
|
+
* nearest samples does not tilt it. With `before` false only the following
|
|
29525
|
+
* points are used (a start tangent); with `after` false only the preceding
|
|
29526
|
+
* ones (an end tangent). The vector points forward along the stroke.
|
|
29527
|
+
*/
|
|
29528
|
+
function windowTangent(pts, index, first, last, window, before, after) {
|
|
29529
|
+
let back = index;
|
|
29530
|
+
if (before) while (back > first && length(sub(pts[index], pts[back])) < window) back--;
|
|
29531
|
+
let ahead = index;
|
|
29532
|
+
if (after) while (ahead < last && length(sub(pts[ahead], pts[index])) < window) ahead++;
|
|
29533
|
+
return normalize(sub(pts[ahead], pts[back]));
|
|
29534
|
+
}
|
|
29535
|
+
function chordLengthParameterize(pts, first, last) {
|
|
29536
|
+
const u = [0];
|
|
29537
|
+
for (let i = first + 1; i <= last; i++) u.push(u[u.length - 1] + length(sub(pts[i], pts[i - 1])));
|
|
29538
|
+
const total = u[u.length - 1] || 1;
|
|
29539
|
+
return u.map((v) => v / total);
|
|
29540
|
+
}
|
|
29541
|
+
function generateBezier(pts, first, last, u, tHat1, tHat2) {
|
|
29542
|
+
const p0 = pts[first];
|
|
29543
|
+
const p3 = pts[last];
|
|
29544
|
+
let c00 = 0, c01 = 0, c11 = 0, x0 = 0, x1 = 0;
|
|
29545
|
+
for (let i = 0; i < u.length; i++) {
|
|
29546
|
+
const a0 = scale(tHat1, B1(u[i]));
|
|
29547
|
+
const a1 = scale(tHat2, B2(u[i]));
|
|
29548
|
+
c00 += dot(a0, a0);
|
|
29549
|
+
c01 += dot(a0, a1);
|
|
29550
|
+
c11 += dot(a1, a1);
|
|
29551
|
+
const tmp = sub(pts[first + i], add(scale(p0, B0(u[i]) + B1(u[i])), scale(p3, B2(u[i]) + B3(u[i]))));
|
|
29552
|
+
x0 += dot(a0, tmp);
|
|
29553
|
+
x1 += dot(a1, tmp);
|
|
29554
|
+
}
|
|
29555
|
+
const det = c00 * c11 - c01 * c01;
|
|
29556
|
+
const alphaL = det === 0 ? 0 : (x0 * c11 - x1 * c01) / det;
|
|
29557
|
+
const alphaR = det === 0 ? 0 : (c00 * x1 - c01 * x0) / det;
|
|
29558
|
+
const segLength = length(sub(p3, p0));
|
|
29559
|
+
const epsilon = 1e-6 * segLength;
|
|
29560
|
+
if (alphaL < epsilon || alphaR < epsilon) {
|
|
29561
|
+
const d = segLength / 3;
|
|
29562
|
+
return [
|
|
29563
|
+
p0,
|
|
29564
|
+
add(p0, scale(tHat1, d)),
|
|
29565
|
+
add(p3, scale(tHat2, d)),
|
|
29566
|
+
p3
|
|
29567
|
+
];
|
|
29568
|
+
}
|
|
29569
|
+
return [
|
|
29570
|
+
p0,
|
|
29571
|
+
add(p0, scale(tHat1, alphaL)),
|
|
29572
|
+
add(p3, scale(tHat2, alphaR)),
|
|
29573
|
+
p3
|
|
29574
|
+
];
|
|
29575
|
+
}
|
|
29576
|
+
function newtonRaphson(b, p, u) {
|
|
29577
|
+
const d = sub(bezierPoint(b, u), p);
|
|
29578
|
+
const d1 = derivative(b, u);
|
|
29579
|
+
const d2 = secondDerivative(b, u);
|
|
29580
|
+
const numerator = dot(d, d1);
|
|
29581
|
+
const denominator = dot(d1, d1) + dot(d, d2);
|
|
29582
|
+
return denominator === 0 ? u : u - numerator / denominator;
|
|
29583
|
+
}
|
|
29584
|
+
function maxError(pts, first, last, b, u) {
|
|
29585
|
+
let split = Math.floor((last - first + 1) / 2) + first;
|
|
29586
|
+
let max = 0;
|
|
29587
|
+
for (let i = first + 1; i < last; i++) {
|
|
29588
|
+
const d = sub(bezierPoint(b, u[i - first]), pts[i]);
|
|
29589
|
+
const e = dot(d, d);
|
|
29590
|
+
if (e >= max) {
|
|
29591
|
+
max = e;
|
|
29592
|
+
split = i;
|
|
29593
|
+
}
|
|
29594
|
+
}
|
|
29595
|
+
return {
|
|
29596
|
+
error: max,
|
|
29597
|
+
split
|
|
29598
|
+
};
|
|
29599
|
+
}
|
|
29600
|
+
function fitCubic(pts, first, last, tHat1, tHat2, errorSq, window, out) {
|
|
29601
|
+
if (last - first === 1) {
|
|
29602
|
+
const d = length(sub(pts[last], pts[first])) / 3;
|
|
29603
|
+
out.push({
|
|
29604
|
+
bez: [
|
|
29605
|
+
pts[first],
|
|
29606
|
+
add(pts[first], scale(tHat1, d)),
|
|
29607
|
+
add(pts[last], scale(tHat2, d)),
|
|
29608
|
+
pts[last]
|
|
29609
|
+
],
|
|
29610
|
+
first,
|
|
29611
|
+
last,
|
|
29612
|
+
u: [0, 1]
|
|
29613
|
+
});
|
|
29614
|
+
return;
|
|
29615
|
+
}
|
|
29616
|
+
let u = chordLengthParameterize(pts, first, last);
|
|
29617
|
+
let bez = generateBezier(pts, first, last, u, tHat1, tHat2);
|
|
29618
|
+
let { error, split } = maxError(pts, first, last, bez, u);
|
|
29619
|
+
if (error < errorSq) {
|
|
29620
|
+
out.push({
|
|
29621
|
+
bez,
|
|
29622
|
+
first,
|
|
29623
|
+
last,
|
|
29624
|
+
u
|
|
29625
|
+
});
|
|
29626
|
+
return;
|
|
29627
|
+
}
|
|
29628
|
+
if (error < errorSq * 4) for (let i = 0; i < 4; i++) {
|
|
29629
|
+
const prime = u.map((v, k) => {
|
|
29630
|
+
const p = newtonRaphson(bez, pts[first + k], v);
|
|
29631
|
+
return Number.isFinite(p) ? Math.min(Math.max(p, 0), 1) : v;
|
|
29632
|
+
});
|
|
29633
|
+
bez = generateBezier(pts, first, last, prime, tHat1, tHat2);
|
|
29634
|
+
({error, split} = maxError(pts, first, last, bez, prime));
|
|
29635
|
+
u = prime;
|
|
29636
|
+
if (error < errorSq) {
|
|
29637
|
+
out.push({
|
|
29638
|
+
bez,
|
|
29639
|
+
first,
|
|
29640
|
+
last,
|
|
29641
|
+
u
|
|
29642
|
+
});
|
|
29643
|
+
return;
|
|
29644
|
+
}
|
|
29645
|
+
}
|
|
29646
|
+
const center = windowTangent(pts, split, first, last, window, true, true);
|
|
29647
|
+
const back = scale(center, -1);
|
|
29648
|
+
fitCubic(pts, first, split, tHat1, back, errorSq, window, out);
|
|
29649
|
+
fitCubic(pts, split, last, center, tHat2, errorSq, window, out);
|
|
29650
|
+
}
|
|
29651
|
+
/**
|
|
29652
|
+
* Fits cubic Bézier segments to a polyline so that no input point is farther
|
|
29653
|
+
* than `tolerance` from the curve. Needs at least two points, none repeated
|
|
29654
|
+
* consecutively.
|
|
29655
|
+
*/
|
|
29656
|
+
function fitCurve(points, tolerance) {
|
|
29657
|
+
if (points.length < 2) return [];
|
|
29658
|
+
const out = [];
|
|
29659
|
+
const last = points.length - 1;
|
|
29660
|
+
const window = tolerance * 3;
|
|
29661
|
+
fitCubic(points, 0, last, windowTangent(points, 0, 0, last, window, false, true), scale(windowTangent(points, last, 0, last, window, true, false), -1), tolerance * tolerance, window, out);
|
|
29662
|
+
return out;
|
|
29663
|
+
}
|
|
29664
|
+
//#endregion
|
|
29665
|
+
//#region src/actions/brush-tool/stages/finalize.ts
|
|
29666
|
+
/** Size of one screen pixel in canvas units at a zoom level. */
|
|
29667
|
+
const pixelSize = (zoom) => zoom > 0 && Number.isFinite(zoom) ? 1 / zoom : 1;
|
|
29668
|
+
/**
|
|
29669
|
+
* Tolerance applied when a stroke is finished, in canvas units. The legacy
|
|
29670
|
+
* value is the tool-level tolerance in canvas units; with the `screen` unit the
|
|
29671
|
+
* configured pixels are converted with the zoom in force when the stroke ends,
|
|
29672
|
+
* so the same setting gives the same on-screen fidelity at any zoom.
|
|
29673
|
+
*/
|
|
29674
|
+
function resolveFinalizeTolerance(cfg, toolTolerance, zoom = 1) {
|
|
29675
|
+
const tolerance = cfg.tolerance ?? toolTolerance;
|
|
29676
|
+
return cfg.toleranceUnit === "screen" ? tolerance * pixelSize(zoom) : tolerance;
|
|
29677
|
+
}
|
|
29678
|
+
/** Distance from `p` to the segment, and where along it the closest point is. */
|
|
29679
|
+
function toSegment(p, { a, b }) {
|
|
29680
|
+
const dx = b.x - a.x;
|
|
29681
|
+
const dy = b.y - a.y;
|
|
29682
|
+
const lengthSq = dx * dx + dy * dy;
|
|
29683
|
+
const t = lengthSq ? Math.min(1, Math.max(0, ((p.x - a.x) * dx + (p.y - a.y) * dy) / lengthSq)) : 0;
|
|
29684
|
+
return {
|
|
29685
|
+
distance: Math.hypot(p.x - (a.x + dx * t), p.y - (a.y + dy * t)),
|
|
29686
|
+
t
|
|
29687
|
+
};
|
|
29688
|
+
}
|
|
29689
|
+
/**
|
|
29690
|
+
* Douglas-Peucker simplification that also keeps pressure detail: a point is
|
|
29691
|
+
* kept when it is off the straight path by more than `tolerance`, or when its
|
|
29692
|
+
* pressure differs from the pressure interpolated along that path by more than
|
|
29693
|
+
* `tolerance / pressureWeight`.
|
|
29694
|
+
*/
|
|
29695
|
+
function simplifyWithPressure(points, tolerance, pressureWeight) {
|
|
29696
|
+
if (points.length <= 2) return points;
|
|
29697
|
+
const keep = new Uint8Array(points.length);
|
|
29698
|
+
keep[0] = keep[points.length - 1] = 1;
|
|
29699
|
+
const stack = [[0, points.length - 1]];
|
|
29700
|
+
while (stack.length > 0) {
|
|
29701
|
+
const [first, last] = stack.pop();
|
|
29702
|
+
let worst = 0;
|
|
29703
|
+
let index = -1;
|
|
29704
|
+
for (let i = first + 1; i < last; i++) {
|
|
29705
|
+
const { distance, t } = toSegment(points[i], {
|
|
29706
|
+
a: points[first],
|
|
29707
|
+
b: points[last]
|
|
29708
|
+
});
|
|
29709
|
+
const expected = points[first].pressure + (points[last].pressure - points[first].pressure) * t;
|
|
29710
|
+
const error = Math.max(distance, Math.abs(points[i].pressure - expected) * pressureWeight);
|
|
29711
|
+
if (error > worst) {
|
|
29712
|
+
worst = error;
|
|
29713
|
+
index = i;
|
|
29714
|
+
}
|
|
29715
|
+
}
|
|
29716
|
+
if (worst > tolerance && index !== -1) {
|
|
29717
|
+
keep[index] = 1;
|
|
29718
|
+
stack.push([first, index], [index, last]);
|
|
29719
|
+
}
|
|
29720
|
+
}
|
|
29721
|
+
return points.filter((_, i) => keep[i] === 1);
|
|
29722
|
+
}
|
|
29723
|
+
/** Pressure at curve parameter `t` of a segment, interpolated between input points. */
|
|
29724
|
+
function pressureAt(seg, points, t) {
|
|
29725
|
+
let prev = seg.u[0];
|
|
29726
|
+
for (let k = 1; k < seg.u.length; k++) {
|
|
29727
|
+
const next = Math.max(seg.u[k], prev);
|
|
29728
|
+
if (t <= next || k === seg.u.length - 1) {
|
|
29729
|
+
const span = next - prev;
|
|
29730
|
+
const f = span > 0 ? Math.min(1, Math.max(0, (t - prev) / span)) : 0;
|
|
29731
|
+
const p0 = points[seg.first + k - 1].pressure;
|
|
29732
|
+
return p0 + (points[seg.first + k].pressure - p0) * f;
|
|
29733
|
+
}
|
|
29734
|
+
prev = next;
|
|
29735
|
+
}
|
|
29736
|
+
return points[seg.last].pressure;
|
|
29737
|
+
}
|
|
29738
|
+
/**
|
|
29739
|
+
* Fits a smooth curve through the stroke and samples it back to points, so the
|
|
29740
|
+
* saved format stays `{x, y, pressure}`. `tolerance` is the largest distance
|
|
29741
|
+
* between the curve and an input point; `spacing` is the distance between
|
|
29742
|
+
* sampled points. Both are in canvas units. The first and last points are kept
|
|
29743
|
+
* exactly.
|
|
29744
|
+
*/
|
|
29745
|
+
function fitAndResample(points, tolerance, spacing) {
|
|
29746
|
+
if (points.length < 3 || !(tolerance > 0)) return points;
|
|
29747
|
+
const distinct = points.filter((p, i) => i === 0 || p.x !== points[i - 1].x || p.y !== points[i - 1].y);
|
|
29748
|
+
if (distinct.length < 3) return points;
|
|
29749
|
+
const segments = fitCurve(distinct, tolerance);
|
|
29750
|
+
if (segments.length === 0) return points;
|
|
29751
|
+
const out = [];
|
|
29752
|
+
for (const seg of segments) {
|
|
29753
|
+
let arc = 0;
|
|
29754
|
+
let prev = seg.bez[0];
|
|
29755
|
+
for (let k = 1; k <= 16; k++) {
|
|
29756
|
+
const q = bezierPoint(seg.bez, k / 16);
|
|
29757
|
+
arc += Math.hypot(q.x - prev.x, q.y - prev.y);
|
|
29758
|
+
prev = q;
|
|
29759
|
+
}
|
|
29760
|
+
const steps = Math.max(1, Math.ceil(arc / Math.max(spacing, 1e-6)));
|
|
29761
|
+
for (let k = 0; k < steps; k++) {
|
|
29762
|
+
const t = k / steps;
|
|
29763
|
+
const q = k === 0 ? seg.bez[0] : bezierPoint(seg.bez, t);
|
|
29764
|
+
out.push({
|
|
29765
|
+
x: q.x,
|
|
29766
|
+
y: q.y,
|
|
29767
|
+
pressure: pressureAt(seg, distinct, t)
|
|
29768
|
+
});
|
|
29769
|
+
}
|
|
29770
|
+
}
|
|
29771
|
+
const end = distinct[distinct.length - 1];
|
|
29772
|
+
out.push({
|
|
29773
|
+
x: end.x,
|
|
29774
|
+
y: end.y,
|
|
29775
|
+
pressure: end.pressure
|
|
29776
|
+
});
|
|
29777
|
+
return out;
|
|
29778
|
+
}
|
|
29779
|
+
/** Reduces a finished stroke's points as selected by the profile. */
|
|
29780
|
+
function finalizePoints(points, cfg, toolTolerance, zoom = 1) {
|
|
29781
|
+
const tolerance = resolveFinalizeTolerance(cfg, toolTolerance, zoom);
|
|
29782
|
+
switch (cfg.mode) {
|
|
29783
|
+
case "none": return points;
|
|
29784
|
+
case "dpPressure": return simplifyWithPressure(points, tolerance, cfg.toleranceUnit === "screen" ? cfg.pressureWeight * pixelSize(zoom) : cfg.pressureWeight);
|
|
29785
|
+
case "bezierFit": return fitAndResample(points, tolerance, cfg.resampleSpacingPx * pixelSize(zoom));
|
|
29786
|
+
default: return simplify(points, tolerance, true);
|
|
29787
|
+
}
|
|
29788
|
+
}
|
|
29789
|
+
//#endregion
|
|
29790
|
+
//#region src/actions/brush-tool/profiles.ts
|
|
29791
|
+
/**
|
|
29792
|
+
* Coarse platform of the running browser: `ipad`, `ios`, `mac`, `windows`,
|
|
29793
|
+
* `android`, `linux` or `unknown`. iPadOS reports a desktop Mac user agent, so
|
|
29794
|
+
* a Mac with multi-touch is an iPad.
|
|
29795
|
+
*/
|
|
29796
|
+
function detectBrushPlatform(nav) {
|
|
29797
|
+
const source = nav === void 0 ? typeof navigator === "undefined" ? null : navigator : nav;
|
|
29798
|
+
if (!source) return "unknown";
|
|
29799
|
+
const ua = source.userAgent ?? "";
|
|
29800
|
+
const platform = source.platform ?? "";
|
|
29801
|
+
if (/iPad/i.test(ua)) return "ipad";
|
|
29802
|
+
if (/iPhone|iPod/i.test(ua)) return "ios";
|
|
29803
|
+
if (/Mac/i.test(platform) && (source.maxTouchPoints ?? 0) > 1) return "ipad";
|
|
29804
|
+
if (/Android/i.test(ua)) return "android";
|
|
29805
|
+
if (/Win/i.test(platform) || /Windows/i.test(ua)) return "windows";
|
|
29806
|
+
if (/Mac/i.test(platform) || /Macintosh/i.test(ua)) return "mac";
|
|
29807
|
+
if (/Linux|X11/i.test(platform) || /Linux|X11/i.test(ua)) return "linux";
|
|
29808
|
+
return "unknown";
|
|
29809
|
+
}
|
|
29810
|
+
const isProfileKey = (key) => BRUSH_INPUT_PROFILE_KEYS.includes(key);
|
|
29811
|
+
/**
|
|
29812
|
+
* Profile chosen from the pointer type and platform. A pen on an iPad is an
|
|
29813
|
+
* Apple Pencil; any other pen is a desktop pen. Unknown pointer types behave
|
|
29814
|
+
* like a mouse.
|
|
29815
|
+
*/
|
|
29816
|
+
function automaticBrushProfileKey(info) {
|
|
29817
|
+
switch (info.pointerType) {
|
|
29818
|
+
case "pen": return info.platform === "ipad" ? "pen.applePencil" : "pen.desktop";
|
|
29819
|
+
case "touch": return "touch";
|
|
29820
|
+
default: return "mouse";
|
|
29821
|
+
}
|
|
29822
|
+
}
|
|
29823
|
+
/**
|
|
29824
|
+
* Resolves the input profile for a stroke. A forced profile wins, then the
|
|
29825
|
+
* application's resolver, then the automatic choice. A resolver that throws or
|
|
29826
|
+
* returns an unknown key is ignored.
|
|
29827
|
+
*/
|
|
29828
|
+
function resolveBrushInputProfileKey(info, cfg) {
|
|
29829
|
+
if (isProfileKey(cfg.forceProfile)) return cfg.forceProfile;
|
|
29830
|
+
if (cfg.resolveProfile) try {
|
|
29831
|
+
const key = cfg.resolveProfile(info);
|
|
29832
|
+
if (isProfileKey(key)) return key;
|
|
29833
|
+
} catch {}
|
|
29834
|
+
return automaticBrushProfileKey(info);
|
|
29835
|
+
}
|
|
29836
|
+
//#endregion
|
|
29837
|
+
//#region src/actions/brush-tool/tunables/descriptors.ts
|
|
29838
|
+
const profile = (key, spec) => ({
|
|
29839
|
+
key,
|
|
29840
|
+
scope: "profile",
|
|
29841
|
+
path: key.split("."),
|
|
29842
|
+
...spec
|
|
29843
|
+
});
|
|
29844
|
+
const look = (key, spec) => ({
|
|
29845
|
+
key,
|
|
29846
|
+
scope: "look",
|
|
29847
|
+
path: key.split("."),
|
|
29848
|
+
...spec
|
|
29849
|
+
});
|
|
29850
|
+
/** Every setting accepted by an input profile. */
|
|
29851
|
+
const BRUSH_PROFILE_TUNABLES = [
|
|
29852
|
+
profile("capture.source", {
|
|
29853
|
+
stage: "capture",
|
|
29854
|
+
group: "Capture",
|
|
29855
|
+
kind: "capture",
|
|
29856
|
+
type: "enum",
|
|
29857
|
+
default: "legacy",
|
|
29858
|
+
options: ["legacy", "coalesced"],
|
|
29859
|
+
help: "legacy keeps one position per event; coalesced keeps every batched pen sample with its own position."
|
|
29860
|
+
}),
|
|
29861
|
+
profile("capture.clock", {
|
|
29862
|
+
stage: "capture",
|
|
29863
|
+
group: "Capture",
|
|
29864
|
+
kind: "capture",
|
|
29865
|
+
type: "enum",
|
|
29866
|
+
default: "now",
|
|
29867
|
+
options: ["now", "event"],
|
|
29868
|
+
help: "Time base of time-dependent filters: processing time or the time reported by each sample."
|
|
29869
|
+
}),
|
|
29870
|
+
profile("capture.minMovePx", {
|
|
29871
|
+
stage: "capture",
|
|
29872
|
+
group: "Capture",
|
|
29873
|
+
kind: "capture",
|
|
29874
|
+
type: "number",
|
|
29875
|
+
default: 0,
|
|
29876
|
+
min: 0,
|
|
29877
|
+
max: 3,
|
|
29878
|
+
step: .1,
|
|
29879
|
+
unit: "px",
|
|
29880
|
+
help: "Samples closer than this to the last recorded one are skipped."
|
|
29881
|
+
}),
|
|
29882
|
+
profile("capture.predicted", {
|
|
29883
|
+
stage: "capture",
|
|
29884
|
+
group: "Capture",
|
|
29885
|
+
kind: "capture",
|
|
29886
|
+
type: "enum",
|
|
29887
|
+
default: "tail",
|
|
29888
|
+
options: ["off", "tail"],
|
|
29889
|
+
help: "Draw the browser-predicted tail ahead of the pen to hide latency."
|
|
29890
|
+
}),
|
|
29891
|
+
profile("capture.event", {
|
|
29892
|
+
stage: "capture",
|
|
29893
|
+
group: "Capture",
|
|
29894
|
+
kind: "capture",
|
|
29895
|
+
type: "enum",
|
|
29896
|
+
default: "pointermove",
|
|
29897
|
+
options: ["pointermove", "pointerrawupdate"],
|
|
29898
|
+
help: "Event that delivers samples. pointerrawupdate lowers latency where supported.",
|
|
29899
|
+
advanced: true
|
|
29900
|
+
}),
|
|
29901
|
+
profile("pressure.zero", {
|
|
29902
|
+
stage: "pressure",
|
|
29903
|
+
group: "Pressure",
|
|
29904
|
+
kind: "capture",
|
|
29905
|
+
type: "enum",
|
|
29906
|
+
default: "legacy",
|
|
29907
|
+
options: [
|
|
29908
|
+
"legacy",
|
|
29909
|
+
"keep",
|
|
29910
|
+
"holdLast"
|
|
29911
|
+
],
|
|
29912
|
+
help: "How a pen pressure of 0 is treated: legacy replaces it with 0.5, keep records it, holdLast repeats the last non-zero value."
|
|
29913
|
+
}),
|
|
29914
|
+
profile("pressure.filter", {
|
|
29915
|
+
stage: "pressure",
|
|
29916
|
+
group: "Pressure",
|
|
29917
|
+
kind: "capture",
|
|
29918
|
+
type: "enum",
|
|
29919
|
+
default: "legacy",
|
|
29920
|
+
options: [
|
|
29921
|
+
"legacy",
|
|
29922
|
+
"timeEma",
|
|
29923
|
+
"oneEuro",
|
|
29924
|
+
"none"
|
|
29925
|
+
],
|
|
29926
|
+
help: "Pressure smoothing: the legacy velocity-adaptive filter, a time-based one, the One Euro filter, or none."
|
|
29927
|
+
}),
|
|
29928
|
+
profile("pressure.timeConstantMs", {
|
|
29929
|
+
stage: "pressure",
|
|
29930
|
+
group: "Pressure",
|
|
29931
|
+
kind: "capture",
|
|
29932
|
+
type: "number",
|
|
29933
|
+
default: 40,
|
|
29934
|
+
min: 1,
|
|
29935
|
+
max: 500,
|
|
29936
|
+
step: 1,
|
|
29937
|
+
unit: "ms",
|
|
29938
|
+
help: "Time constant of the timeEma filter; larger is smoother and slower."
|
|
29939
|
+
}),
|
|
29940
|
+
profile("pressure.oneEuro.minCutoff", {
|
|
29941
|
+
stage: "pressure",
|
|
29942
|
+
group: "Pressure",
|
|
29943
|
+
kind: "capture",
|
|
29944
|
+
type: "number",
|
|
29945
|
+
default: 1,
|
|
29946
|
+
min: .01,
|
|
29947
|
+
max: 10,
|
|
29948
|
+
step: .01,
|
|
29949
|
+
unit: "Hz",
|
|
29950
|
+
help: "One Euro minimum cutoff: lower removes more jitter at slow speed.",
|
|
29951
|
+
advanced: true
|
|
29952
|
+
}),
|
|
29953
|
+
profile("pressure.oneEuro.beta", {
|
|
29954
|
+
stage: "pressure",
|
|
29955
|
+
group: "Pressure",
|
|
29956
|
+
kind: "capture",
|
|
29957
|
+
type: "number",
|
|
29958
|
+
default: .01,
|
|
29959
|
+
min: 0,
|
|
29960
|
+
max: 1,
|
|
29961
|
+
step: .001,
|
|
29962
|
+
help: "One Euro speed coefficient: higher removes more lag at high speed.",
|
|
29963
|
+
advanced: true
|
|
29964
|
+
}),
|
|
29965
|
+
profile("pressure.floor", {
|
|
29966
|
+
stage: "pressure",
|
|
29967
|
+
group: "Pressure",
|
|
29968
|
+
kind: "capture",
|
|
29969
|
+
type: "number",
|
|
29970
|
+
default: .15,
|
|
29971
|
+
min: 0,
|
|
29972
|
+
max: .5,
|
|
29973
|
+
step: .01,
|
|
29974
|
+
unit: "fraction",
|
|
29975
|
+
help: "Lowest pressure that is recorded; prevents invisible strokes."
|
|
29976
|
+
}),
|
|
29977
|
+
profile("pressure.start", {
|
|
29978
|
+
stage: "pressure",
|
|
29979
|
+
group: "Pressure",
|
|
29980
|
+
kind: "capture",
|
|
29981
|
+
type: "enum",
|
|
29982
|
+
default: "backfill",
|
|
29983
|
+
options: [
|
|
29984
|
+
"backfill",
|
|
29985
|
+
"average",
|
|
29986
|
+
"none"
|
|
29987
|
+
],
|
|
29988
|
+
help: "First point: backfill takes the next real sample, average takes the mean of the first movement readings, none keeps the contact reading."
|
|
29989
|
+
}),
|
|
29990
|
+
profile("pressure.startSamples", {
|
|
29991
|
+
stage: "pressure",
|
|
29992
|
+
group: "Pressure",
|
|
29993
|
+
kind: "capture",
|
|
29994
|
+
type: "number",
|
|
29995
|
+
default: 3,
|
|
29996
|
+
min: 1,
|
|
29997
|
+
max: 10,
|
|
29998
|
+
step: 1,
|
|
29999
|
+
integer: true,
|
|
30000
|
+
unit: "count",
|
|
30001
|
+
help: "Number of readings averaged by the average start policy.",
|
|
30002
|
+
advanced: true
|
|
30003
|
+
}),
|
|
30004
|
+
profile("pressure.end", {
|
|
30005
|
+
stage: "pressure",
|
|
30006
|
+
group: "Pressure",
|
|
30007
|
+
kind: "capture",
|
|
30008
|
+
type: "enum",
|
|
30009
|
+
default: "backfill",
|
|
30010
|
+
options: ["backfill", "none"],
|
|
30011
|
+
help: "Last point: backfill takes the second-to-last pressure, none keeps the lift-off reading."
|
|
30012
|
+
}),
|
|
30013
|
+
profile("stabilizer.type", {
|
|
30014
|
+
stage: "stabilizer",
|
|
30015
|
+
group: "Stabilizer",
|
|
30016
|
+
kind: "capture",
|
|
30017
|
+
type: "enum",
|
|
30018
|
+
default: "none",
|
|
30019
|
+
options: [
|
|
30020
|
+
"none",
|
|
30021
|
+
"oneEuro",
|
|
30022
|
+
"pulledString",
|
|
30023
|
+
"streamline"
|
|
30024
|
+
],
|
|
30025
|
+
help: "Smooths the pen position while drawing."
|
|
30026
|
+
}),
|
|
30027
|
+
profile("stabilizer.minCutoff", {
|
|
30028
|
+
stage: "stabilizer",
|
|
30029
|
+
group: "Stabilizer",
|
|
30030
|
+
kind: "capture",
|
|
30031
|
+
type: "number",
|
|
30032
|
+
default: 3,
|
|
30033
|
+
min: .1,
|
|
30034
|
+
max: 20,
|
|
30035
|
+
step: .1,
|
|
30036
|
+
unit: "Hz",
|
|
30037
|
+
help: "One Euro minimum cutoff: lower removes more jitter at slow speed."
|
|
30038
|
+
}),
|
|
30039
|
+
profile("stabilizer.beta", {
|
|
30040
|
+
stage: "stabilizer",
|
|
30041
|
+
group: "Stabilizer",
|
|
30042
|
+
kind: "capture",
|
|
30043
|
+
type: "number",
|
|
30044
|
+
default: .01,
|
|
30045
|
+
min: 0,
|
|
30046
|
+
max: 1,
|
|
30047
|
+
step: .001,
|
|
30048
|
+
help: "One Euro speed coefficient: higher removes more lag at high speed."
|
|
30049
|
+
}),
|
|
30050
|
+
profile("stabilizer.radiusPx", {
|
|
30051
|
+
stage: "stabilizer",
|
|
30052
|
+
group: "Stabilizer",
|
|
30053
|
+
kind: "capture",
|
|
30054
|
+
type: "number",
|
|
30055
|
+
default: 4,
|
|
30056
|
+
min: 0,
|
|
30057
|
+
max: 40,
|
|
30058
|
+
step: .5,
|
|
30059
|
+
unit: "px",
|
|
30060
|
+
help: "Pulled string leash length: the pen must move this far before the line follows."
|
|
30061
|
+
}),
|
|
30062
|
+
profile("stabilizer.amount", {
|
|
30063
|
+
stage: "stabilizer",
|
|
30064
|
+
group: "Stabilizer",
|
|
30065
|
+
kind: "capture",
|
|
30066
|
+
type: "number",
|
|
30067
|
+
default: .3,
|
|
30068
|
+
min: 0,
|
|
30069
|
+
max: 1,
|
|
30070
|
+
step: .01,
|
|
30071
|
+
unit: "fraction",
|
|
30072
|
+
help: "Streamline strength: 0 follows the pen exactly, 1 is the strongest smoothing."
|
|
30073
|
+
}),
|
|
30074
|
+
profile("stabilizer.zoomAware", {
|
|
30075
|
+
stage: "stabilizer",
|
|
30076
|
+
group: "Stabilizer",
|
|
30077
|
+
kind: "capture",
|
|
30078
|
+
type: "boolean",
|
|
30079
|
+
default: false,
|
|
30080
|
+
help: "Interpret the distances in screen pixels, so the effect on screen is the same at every zoom level. Off: canvas units."
|
|
30081
|
+
}),
|
|
30082
|
+
profile("stabilizer.catchUpOnEnd", {
|
|
30083
|
+
stage: "stabilizer",
|
|
30084
|
+
group: "Stabilizer",
|
|
30085
|
+
kind: "capture",
|
|
30086
|
+
type: "boolean",
|
|
30087
|
+
default: true,
|
|
30088
|
+
help: "Complete the stroke at the position where the pen was lifted."
|
|
30089
|
+
}),
|
|
30090
|
+
profile("finalize.mode", {
|
|
30091
|
+
stage: "finalize",
|
|
30092
|
+
group: "Finish",
|
|
30093
|
+
kind: "capture",
|
|
30094
|
+
type: "enum",
|
|
30095
|
+
default: "dp",
|
|
30096
|
+
options: [
|
|
30097
|
+
"dp",
|
|
30098
|
+
"dpPressure",
|
|
30099
|
+
"bezierFit",
|
|
30100
|
+
"none"
|
|
30101
|
+
],
|
|
30102
|
+
help: "How points are reduced when the stroke ends: simplification, simplification keeping pressure detail, curve fitting, or nothing."
|
|
30103
|
+
}),
|
|
30104
|
+
profile("finalize.tolerance", {
|
|
30105
|
+
stage: "finalize",
|
|
30106
|
+
group: "Finish",
|
|
30107
|
+
kind: "capture",
|
|
30108
|
+
type: "number",
|
|
30109
|
+
default: null,
|
|
30110
|
+
nullable: true,
|
|
30111
|
+
min: 0,
|
|
30112
|
+
max: 20,
|
|
30113
|
+
step: .05,
|
|
30114
|
+
help: "Tolerance of the finishing step. Empty uses the tool-level tolerance."
|
|
30115
|
+
}),
|
|
30116
|
+
profile("finalize.toleranceUnit", {
|
|
30117
|
+
stage: "finalize",
|
|
30118
|
+
group: "Finish",
|
|
30119
|
+
kind: "capture",
|
|
30120
|
+
type: "enum",
|
|
30121
|
+
default: "canvas",
|
|
30122
|
+
options: ["canvas", "screen"],
|
|
30123
|
+
help: "canvas: tolerance in canvas units. screen: in pixels, converted with the zoom when the stroke ends."
|
|
30124
|
+
}),
|
|
30125
|
+
profile("finalize.pressureWeight", {
|
|
30126
|
+
stage: "finalize",
|
|
30127
|
+
group: "Finish",
|
|
30128
|
+
kind: "capture",
|
|
30129
|
+
type: "number",
|
|
30130
|
+
default: 20,
|
|
30131
|
+
min: 0,
|
|
30132
|
+
max: 200,
|
|
30133
|
+
step: 1,
|
|
30134
|
+
help: "Distance units per pressure unit when simplification keeps pressure detail.",
|
|
30135
|
+
advanced: true
|
|
30136
|
+
}),
|
|
30137
|
+
profile("finalize.resampleSpacingPx", {
|
|
30138
|
+
stage: "finalize",
|
|
30139
|
+
group: "Finish",
|
|
30140
|
+
kind: "capture",
|
|
30141
|
+
type: "number",
|
|
30142
|
+
default: 2,
|
|
30143
|
+
min: .5,
|
|
30144
|
+
max: 20,
|
|
30145
|
+
step: .5,
|
|
30146
|
+
unit: "px",
|
|
30147
|
+
help: "Spacing of the points a fitted curve is resampled to.",
|
|
30148
|
+
advanced: true
|
|
30149
|
+
}),
|
|
30150
|
+
profile("touchGuard.waitMs", {
|
|
30151
|
+
stage: "touch",
|
|
30152
|
+
group: "Finger",
|
|
30153
|
+
kind: "capture",
|
|
30154
|
+
type: "number",
|
|
30155
|
+
default: 0,
|
|
30156
|
+
min: 0,
|
|
30157
|
+
max: 500,
|
|
30158
|
+
step: 10,
|
|
30159
|
+
unit: "ms",
|
|
30160
|
+
help: "Wait before a finger stroke is committed, so a second finger can cancel it."
|
|
30161
|
+
}),
|
|
30162
|
+
profile("touchGuard.movePx", {
|
|
30163
|
+
stage: "touch",
|
|
30164
|
+
group: "Finger",
|
|
30165
|
+
kind: "capture",
|
|
30166
|
+
type: "number",
|
|
30167
|
+
default: 0,
|
|
30168
|
+
min: 0,
|
|
30169
|
+
max: 40,
|
|
30170
|
+
step: 1,
|
|
30171
|
+
unit: "px",
|
|
30172
|
+
help: "Movement that commits the stroke before the wait ends."
|
|
30173
|
+
}),
|
|
30174
|
+
profile("touchGuard.cancelOnSecondTouch", {
|
|
30175
|
+
stage: "touch",
|
|
30176
|
+
group: "Finger",
|
|
30177
|
+
kind: "capture",
|
|
30178
|
+
type: "boolean",
|
|
30179
|
+
default: false,
|
|
30180
|
+
help: "Cancel the stroke when a second finger lands during the wait."
|
|
30181
|
+
}),
|
|
30182
|
+
profile("touchGuard.ignoreAfterPen", {
|
|
30183
|
+
stage: "touch",
|
|
30184
|
+
group: "Finger",
|
|
30185
|
+
kind: "capture",
|
|
30186
|
+
type: "boolean",
|
|
30187
|
+
default: false,
|
|
30188
|
+
help: "Finger input does not draw once a pen has been used."
|
|
30189
|
+
})
|
|
30190
|
+
];
|
|
30191
|
+
/** Every setting accepted by a stroke look. */
|
|
30192
|
+
const BRUSH_LOOK_TUNABLES = [
|
|
30193
|
+
look("outline", {
|
|
30194
|
+
stage: "outline",
|
|
30195
|
+
group: "Outline",
|
|
30196
|
+
kind: "draw",
|
|
30197
|
+
type: "enum",
|
|
30198
|
+
default: "legacy",
|
|
30199
|
+
options: [
|
|
30200
|
+
"legacy",
|
|
30201
|
+
"round",
|
|
30202
|
+
"freehand"
|
|
30203
|
+
],
|
|
30204
|
+
help: "Shape of the stroke: legacy ribbon, round (discs and segments, no spikes) or freehand."
|
|
30205
|
+
}),
|
|
30206
|
+
look("curve", {
|
|
30207
|
+
stage: "curve",
|
|
30208
|
+
group: "Curve",
|
|
30209
|
+
kind: "draw",
|
|
30210
|
+
type: "enum",
|
|
30211
|
+
default: "uniform",
|
|
30212
|
+
options: [
|
|
30213
|
+
"uniform",
|
|
30214
|
+
"centripetal",
|
|
30215
|
+
"none"
|
|
30216
|
+
],
|
|
30217
|
+
help: "Curve through the points. centripetal does not overshoot when points are unevenly spaced."
|
|
30218
|
+
}),
|
|
30219
|
+
look("pressureInterp", {
|
|
30220
|
+
stage: "curve",
|
|
30221
|
+
group: "Curve",
|
|
30222
|
+
kind: "draw",
|
|
30223
|
+
type: "enum",
|
|
30224
|
+
default: "catmullRom",
|
|
30225
|
+
options: ["catmullRom", "linear"],
|
|
30226
|
+
help: "How pressure is interpolated between points. linear never bulges."
|
|
30227
|
+
}),
|
|
30228
|
+
look("zoomAware", {
|
|
30229
|
+
stage: "detail",
|
|
30230
|
+
group: "Detail",
|
|
30231
|
+
kind: "draw",
|
|
30232
|
+
type: "boolean",
|
|
30233
|
+
default: false,
|
|
30234
|
+
help: "Choose spacing and subdivision in screen pixels so smoothness does not depend on zoom."
|
|
30235
|
+
}),
|
|
30236
|
+
look("spacingPx", {
|
|
30237
|
+
stage: "detail",
|
|
30238
|
+
group: "Detail",
|
|
30239
|
+
kind: "draw",
|
|
30240
|
+
type: "number",
|
|
30241
|
+
default: 2,
|
|
30242
|
+
min: .5,
|
|
30243
|
+
max: 10,
|
|
30244
|
+
step: .5,
|
|
30245
|
+
unit: "px",
|
|
30246
|
+
help: "Minimum spacing between control points.",
|
|
30247
|
+
advanced: true
|
|
30248
|
+
}),
|
|
30249
|
+
look("edgePx", {
|
|
30250
|
+
stage: "detail",
|
|
30251
|
+
group: "Detail",
|
|
30252
|
+
kind: "draw",
|
|
30253
|
+
type: "number",
|
|
30254
|
+
default: 1,
|
|
30255
|
+
min: .25,
|
|
30256
|
+
max: 5,
|
|
30257
|
+
step: .25,
|
|
30258
|
+
unit: "px",
|
|
30259
|
+
help: "Detail of the curve. The centripetal curve keeps every edge within a tenth of this of the true curve; the uniform curve uses it as the target length of an edge.",
|
|
30260
|
+
advanced: true
|
|
30261
|
+
}),
|
|
30262
|
+
look("caps", {
|
|
30263
|
+
stage: "ends",
|
|
30264
|
+
group: "Ends",
|
|
30265
|
+
kind: "draw",
|
|
30266
|
+
type: "enum",
|
|
30267
|
+
default: "separate",
|
|
30268
|
+
options: ["separate", "merged"],
|
|
30269
|
+
help: "merged draws the round ends inside the same fill, so translucent colours stay uniform."
|
|
30270
|
+
}),
|
|
30271
|
+
look("width.min", {
|
|
30272
|
+
stage: "width",
|
|
30273
|
+
group: "Width",
|
|
30274
|
+
kind: "draw",
|
|
30275
|
+
type: "number",
|
|
30276
|
+
default: 0,
|
|
30277
|
+
min: 0,
|
|
30278
|
+
max: 1,
|
|
30279
|
+
step: .01,
|
|
30280
|
+
unit: "fraction",
|
|
30281
|
+
help: "Width at pressure 0, as a fraction of the stroke width."
|
|
30282
|
+
}),
|
|
30283
|
+
look("width.max", {
|
|
30284
|
+
stage: "width",
|
|
30285
|
+
group: "Width",
|
|
30286
|
+
kind: "draw",
|
|
30287
|
+
type: "number",
|
|
30288
|
+
default: 1,
|
|
30289
|
+
min: 0,
|
|
30290
|
+
max: 1,
|
|
30291
|
+
step: .01,
|
|
30292
|
+
unit: "fraction",
|
|
30293
|
+
help: "Width at pressure 1, as a fraction of the stroke width."
|
|
30294
|
+
}),
|
|
30295
|
+
look("width.gamma", {
|
|
30296
|
+
stage: "width",
|
|
30297
|
+
group: "Width",
|
|
30298
|
+
kind: "draw",
|
|
30299
|
+
type: "number",
|
|
30300
|
+
default: 1,
|
|
30301
|
+
min: .2,
|
|
30302
|
+
max: 3,
|
|
30303
|
+
step: .05,
|
|
30304
|
+
help: "Response curve: below 1 reacts faster to light pressure, above 1 needs a firmer press."
|
|
30305
|
+
}),
|
|
30306
|
+
look("width.taperStart", {
|
|
30307
|
+
stage: "width",
|
|
30308
|
+
group: "Width",
|
|
30309
|
+
kind: "draw",
|
|
30310
|
+
type: "number",
|
|
30311
|
+
default: 0,
|
|
30312
|
+
min: 0,
|
|
30313
|
+
max: 200,
|
|
30314
|
+
step: 1,
|
|
30315
|
+
unit: "px",
|
|
30316
|
+
help: "Distance over which the start narrows; 0 for none."
|
|
30317
|
+
}),
|
|
30318
|
+
look("width.taperEnd", {
|
|
30319
|
+
stage: "width",
|
|
30320
|
+
group: "Width",
|
|
30321
|
+
kind: "draw",
|
|
30322
|
+
type: "number",
|
|
30323
|
+
default: 0,
|
|
30324
|
+
min: 0,
|
|
30325
|
+
max: 200,
|
|
30326
|
+
step: 1,
|
|
30327
|
+
unit: "px",
|
|
30328
|
+
help: "Distance over which the end narrows; 0 for none."
|
|
30329
|
+
})
|
|
30330
|
+
];
|
|
30331
|
+
/** Every tunable setting. */
|
|
30332
|
+
const BRUSH_TUNABLES = [...BRUSH_PROFILE_TUNABLES, ...BRUSH_LOOK_TUNABLES];
|
|
30333
|
+
/** Finds a tunable by scope and key. */
|
|
30334
|
+
function getBrushTunable(scope, key) {
|
|
30335
|
+
return BRUSH_TUNABLES.find((t) => t.scope === scope && t.key === key);
|
|
30336
|
+
}
|
|
30337
|
+
//#endregion
|
|
30338
|
+
//#region src/actions/brush-tool/tunables/validate.ts
|
|
30339
|
+
/**
|
|
30340
|
+
* Checks a value against a descriptor. Returns a message when the value is
|
|
30341
|
+
* invalid, or `null` when it is accepted.
|
|
30342
|
+
*/
|
|
30343
|
+
function validateBrushTunableValue(tunable, value) {
|
|
30344
|
+
const name = `${tunable.scope} setting "${tunable.key}"`;
|
|
30345
|
+
if (value === null || value === void 0) return tunable.nullable ? null : `${name} cannot be empty`;
|
|
30346
|
+
switch (tunable.type) {
|
|
30347
|
+
case "boolean": return typeof value === "boolean" ? null : `${name} must be true or false`;
|
|
30348
|
+
case "enum": return typeof value === "string" && tunable.options?.includes(value) ? null : `${name} must be one of: ${tunable.options?.join(", ")}`;
|
|
30349
|
+
case "number":
|
|
30350
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return `${name} must be a finite number`;
|
|
30351
|
+
if (tunable.integer && !Number.isInteger(value)) return `${name} must be a whole number`;
|
|
30352
|
+
if (tunable.min !== void 0 && value < tunable.min) return `${name} must be at least ${tunable.min}`;
|
|
30353
|
+
if (tunable.max !== void 0 && value > tunable.max) return `${name} must be at most ${tunable.max}`;
|
|
30354
|
+
return null;
|
|
30355
|
+
}
|
|
30356
|
+
}
|
|
30357
|
+
function collectLeaves(obj, prefix = []) {
|
|
30358
|
+
return Object.entries(obj).flatMap(([k, v]) => v !== null && typeof v === "object" && !Array.isArray(v) ? collectLeaves(v, [...prefix, k]) : [{
|
|
30359
|
+
key: [...prefix, k].join("."),
|
|
30360
|
+
value: v
|
|
30361
|
+
}]);
|
|
30362
|
+
}
|
|
30363
|
+
/**
|
|
30364
|
+
* Validates a partial profile or look against the descriptors. Every leaf must
|
|
30365
|
+
* name a known setting and hold an accepted value. Returns the messages of all
|
|
30366
|
+
* problems found; an empty list means the patch is valid.
|
|
30367
|
+
*/
|
|
30368
|
+
function validateBrushTunablePatch(scope, patch) {
|
|
30369
|
+
if (patch === null || typeof patch !== "object" || Array.isArray(patch)) return [`A ${scope} change must be an object`];
|
|
30370
|
+
const errors = [];
|
|
30371
|
+
for (const { key, value } of collectLeaves(patch)) {
|
|
30372
|
+
const tunable = getBrushTunable(scope, key);
|
|
30373
|
+
if (!tunable) {
|
|
30374
|
+
errors.push(`Unknown ${scope} setting "${key}"`);
|
|
30375
|
+
continue;
|
|
30376
|
+
}
|
|
30377
|
+
const error = validateBrushTunableValue(tunable, value);
|
|
30378
|
+
if (error) errors.push(error);
|
|
30379
|
+
}
|
|
30380
|
+
return errors;
|
|
30381
|
+
}
|
|
30382
|
+
//#endregion
|
|
30383
|
+
//#region src/actions/brush-tool/tunables/merge.ts
|
|
30384
|
+
const isPlain = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
30385
|
+
/** Deep clone of plain data. */
|
|
30386
|
+
function cloneData(value) {
|
|
30387
|
+
if (Array.isArray(value)) return value.map(cloneData);
|
|
30388
|
+
if (isPlain(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneData(v)]));
|
|
30389
|
+
return value;
|
|
30390
|
+
}
|
|
30391
|
+
/** Returns a new object: `base` with every defined value of `patch` merged in. */
|
|
30392
|
+
function mergeData(base, patch) {
|
|
30393
|
+
const result = cloneData(base);
|
|
30394
|
+
if (!isPlain(patch)) return result;
|
|
30395
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
30396
|
+
if (v === void 0) continue;
|
|
30397
|
+
result[k] = isPlain(v) && isPlain(result[k]) ? mergeData(result[k], v) : cloneData(v);
|
|
30398
|
+
}
|
|
30399
|
+
return result;
|
|
30400
|
+
}
|
|
30401
|
+
/** Freezes plain data recursively. */
|
|
30402
|
+
function deepFreeze(value) {
|
|
30403
|
+
if (isPlain(value) || Array.isArray(value)) {
|
|
30404
|
+
for (const v of Object.values(value)) deepFreeze(v);
|
|
30405
|
+
Object.freeze(value);
|
|
30406
|
+
}
|
|
30407
|
+
return value;
|
|
30408
|
+
}
|
|
30409
|
+
//#endregion
|
|
30410
|
+
//#region src/actions/brush-tool/tunables/presets.ts
|
|
30411
|
+
const FIXES = {
|
|
30412
|
+
capture: {
|
|
30413
|
+
source: "coalesced",
|
|
30414
|
+
clock: "event"
|
|
30415
|
+
},
|
|
30416
|
+
pressure: { zero: "keep" }
|
|
30417
|
+
};
|
|
30418
|
+
const withFixes = (patch) => mergeData(FIXES, patch);
|
|
30419
|
+
const PEN_SMOOTH = withFixes({
|
|
30420
|
+
pressure: {
|
|
30421
|
+
filter: "oneEuro",
|
|
30422
|
+
oneEuro: {
|
|
30423
|
+
minCutoff: 1.5,
|
|
30424
|
+
beta: .02
|
|
30425
|
+
},
|
|
30426
|
+
start: "average"
|
|
30427
|
+
},
|
|
30428
|
+
stabilizer: {
|
|
30429
|
+
type: "oneEuro",
|
|
30430
|
+
minCutoff: 6,
|
|
30431
|
+
beta: .05,
|
|
30432
|
+
zoomAware: true
|
|
30433
|
+
},
|
|
30434
|
+
finalize: {
|
|
30435
|
+
mode: "bezierFit",
|
|
30436
|
+
toleranceUnit: "screen",
|
|
30437
|
+
tolerance: 2,
|
|
30438
|
+
resampleSpacingPx: 8
|
|
30439
|
+
}
|
|
30440
|
+
});
|
|
30441
|
+
const PEN_ILLUSTRATOR = withFixes({
|
|
30442
|
+
pressure: {
|
|
30443
|
+
filter: "timeEma",
|
|
30444
|
+
timeConstantMs: 30,
|
|
30445
|
+
start: "average"
|
|
30446
|
+
},
|
|
30447
|
+
stabilizer: {
|
|
30448
|
+
type: "streamline",
|
|
30449
|
+
amount: .25,
|
|
30450
|
+
zoomAware: true
|
|
30451
|
+
},
|
|
30452
|
+
finalize: {
|
|
30453
|
+
mode: "bezierFit",
|
|
30454
|
+
toleranceUnit: "screen",
|
|
30455
|
+
tolerance: 2,
|
|
30456
|
+
resampleSpacingPx: 8
|
|
30457
|
+
}
|
|
30458
|
+
});
|
|
30459
|
+
const TOUCH_GUARD = withFixes({ touchGuard: {
|
|
30460
|
+
waitMs: 100,
|
|
30461
|
+
movePx: 6,
|
|
30462
|
+
cancelOnSecondTouch: true
|
|
30463
|
+
} });
|
|
30464
|
+
const legacy = () => ({
|
|
30465
|
+
id: "legacy",
|
|
30466
|
+
label: "Legacy",
|
|
30467
|
+
description: "Behaviour of earlier releases, point for point.",
|
|
30468
|
+
patch: {}
|
|
30469
|
+
});
|
|
30470
|
+
const fixed = () => ({
|
|
30471
|
+
id: "fixed",
|
|
30472
|
+
label: "Fixed",
|
|
30473
|
+
description: "Keeps every pen sample, uses event time, keeps zero pressure.",
|
|
30474
|
+
patch: FIXES
|
|
30475
|
+
});
|
|
30476
|
+
const penPresets = () => [
|
|
30477
|
+
legacy(),
|
|
30478
|
+
fixed(),
|
|
30479
|
+
{
|
|
30480
|
+
id: "smooth",
|
|
30481
|
+
label: "Smooth",
|
|
30482
|
+
description: "Fixed, plus One Euro smoothing of pressure and position and a Bézier-fitted finish.",
|
|
30483
|
+
patch: PEN_SMOOTH
|
|
30484
|
+
},
|
|
30485
|
+
{
|
|
30486
|
+
id: "illustrator",
|
|
30487
|
+
label: "Illustrator-like",
|
|
30488
|
+
description: "Fixed, plus streamline stabilizing and a Bézier-fitted finish in screen pixels.",
|
|
30489
|
+
patch: PEN_ILLUSTRATOR
|
|
30490
|
+
}
|
|
30491
|
+
];
|
|
30492
|
+
const RAW_PROFILE_PRESETS = {
|
|
30493
|
+
mouse: [
|
|
30494
|
+
legacy(),
|
|
30495
|
+
fixed(),
|
|
30496
|
+
{
|
|
30497
|
+
id: "stabilized",
|
|
30498
|
+
label: "Stabilized",
|
|
30499
|
+
description: "Fixed, plus One Euro position smoothing that follows the zoom and a Bézier-fitted finish.",
|
|
30500
|
+
patch: withFixes({
|
|
30501
|
+
stabilizer: {
|
|
30502
|
+
type: "oneEuro",
|
|
30503
|
+
minCutoff: 2,
|
|
30504
|
+
beta: .01,
|
|
30505
|
+
zoomAware: true
|
|
30506
|
+
},
|
|
30507
|
+
finalize: {
|
|
30508
|
+
mode: "bezierFit",
|
|
30509
|
+
toleranceUnit: "screen",
|
|
30510
|
+
tolerance: 2,
|
|
30511
|
+
resampleSpacingPx: 8
|
|
30512
|
+
}
|
|
30513
|
+
})
|
|
30514
|
+
},
|
|
30515
|
+
{
|
|
30516
|
+
id: "pulledString",
|
|
30517
|
+
label: "Pulled string",
|
|
30518
|
+
description: "Fixed, plus a pulled-string stabilizer.",
|
|
30519
|
+
patch: withFixes({ stabilizer: {
|
|
30520
|
+
type: "pulledString",
|
|
30521
|
+
radiusPx: 6,
|
|
30522
|
+
zoomAware: true
|
|
30523
|
+
} })
|
|
30524
|
+
}
|
|
30525
|
+
],
|
|
30526
|
+
touch: [
|
|
30527
|
+
legacy(),
|
|
30528
|
+
fixed(),
|
|
30529
|
+
{
|
|
30530
|
+
id: "guarded",
|
|
30531
|
+
label: "Guarded",
|
|
30532
|
+
description: "Fixed, plus a short wait that a second finger can cancel.",
|
|
30533
|
+
patch: TOUCH_GUARD
|
|
30534
|
+
},
|
|
30535
|
+
{
|
|
30536
|
+
id: "smooth",
|
|
30537
|
+
label: "Smooth",
|
|
30538
|
+
description: "Guarded, plus One Euro position smoothing and a Bézier-fitted finish.",
|
|
30539
|
+
patch: mergeData(TOUCH_GUARD, {
|
|
30540
|
+
stabilizer: {
|
|
30541
|
+
type: "oneEuro",
|
|
30542
|
+
minCutoff: 3,
|
|
30543
|
+
beta: .02,
|
|
30544
|
+
zoomAware: true
|
|
30545
|
+
},
|
|
30546
|
+
finalize: {
|
|
30547
|
+
mode: "bezierFit",
|
|
30548
|
+
toleranceUnit: "screen",
|
|
30549
|
+
tolerance: 2,
|
|
30550
|
+
resampleSpacingPx: 8
|
|
30551
|
+
}
|
|
30552
|
+
})
|
|
30553
|
+
}
|
|
30554
|
+
],
|
|
30555
|
+
"pen.applePencil": penPresets(),
|
|
30556
|
+
"pen.desktop": penPresets()
|
|
30557
|
+
};
|
|
30558
|
+
const WIDTH_CURVE = { width: {
|
|
30559
|
+
min: .2,
|
|
30560
|
+
gamma: .8
|
|
30561
|
+
} };
|
|
30562
|
+
const ROUND = {
|
|
30563
|
+
...WIDTH_CURVE,
|
|
30564
|
+
outline: "round",
|
|
30565
|
+
caps: "merged",
|
|
30566
|
+
curve: "centripetal",
|
|
30567
|
+
pressureInterp: "linear",
|
|
30568
|
+
zoomAware: true
|
|
30569
|
+
};
|
|
30570
|
+
const RAW_LOOK_PRESETS = [
|
|
30571
|
+
{
|
|
30572
|
+
id: "legacy",
|
|
30573
|
+
label: "Legacy",
|
|
30574
|
+
description: "No look is stored: strokes are drawn as in earlier releases.",
|
|
30575
|
+
patch: null
|
|
30576
|
+
},
|
|
30577
|
+
{
|
|
30578
|
+
id: "widthCurve",
|
|
30579
|
+
label: "Width curve",
|
|
30580
|
+
description: "Legacy outline with a minimum width and a pressure curve.",
|
|
30581
|
+
patch: WIDTH_CURVE
|
|
30582
|
+
},
|
|
30583
|
+
{
|
|
30584
|
+
id: "round",
|
|
30585
|
+
label: "Round",
|
|
30586
|
+
description: "Round outline: no spikes at turns, uniform translucent colour.",
|
|
30587
|
+
patch: ROUND
|
|
30588
|
+
},
|
|
30589
|
+
{
|
|
30590
|
+
id: "freehand",
|
|
30591
|
+
label: "Freehand",
|
|
30592
|
+
description: "Freehand outline.",
|
|
30593
|
+
patch: {
|
|
30594
|
+
...WIDTH_CURVE,
|
|
30595
|
+
outline: "freehand",
|
|
30596
|
+
caps: "merged",
|
|
30597
|
+
zoomAware: true
|
|
30598
|
+
}
|
|
30599
|
+
},
|
|
30600
|
+
{
|
|
30601
|
+
id: "illustrator",
|
|
30602
|
+
label: "Illustrator-like",
|
|
30603
|
+
description: "Round outline with a Size ± Variation style width range.",
|
|
30604
|
+
patch: {
|
|
30605
|
+
...ROUND,
|
|
30606
|
+
width: {
|
|
30607
|
+
min: .4,
|
|
30608
|
+
max: 1,
|
|
30609
|
+
gamma: 1
|
|
30610
|
+
}
|
|
30611
|
+
}
|
|
30612
|
+
}
|
|
30613
|
+
];
|
|
30614
|
+
const RAW_SCENARIOS = [
|
|
30615
|
+
{
|
|
30616
|
+
id: "A",
|
|
30617
|
+
label: "A: legacy",
|
|
30618
|
+
description: "Today's behaviour, identical point for point.",
|
|
30619
|
+
profiles: {
|
|
30620
|
+
mouse: "legacy",
|
|
30621
|
+
touch: "legacy",
|
|
30622
|
+
"pen.applePencil": "legacy",
|
|
30623
|
+
"pen.desktop": "legacy"
|
|
30624
|
+
},
|
|
30625
|
+
look: "legacy"
|
|
30626
|
+
},
|
|
30627
|
+
{
|
|
30628
|
+
id: "B",
|
|
30629
|
+
label: "B: fixes",
|
|
30630
|
+
description: "Every pen sample, event time and zero pressure kept.",
|
|
30631
|
+
profiles: {
|
|
30632
|
+
mouse: "fixed",
|
|
30633
|
+
touch: "fixed",
|
|
30634
|
+
"pen.applePencil": "fixed",
|
|
30635
|
+
"pen.desktop": "fixed"
|
|
30636
|
+
},
|
|
30637
|
+
look: "legacy"
|
|
30638
|
+
},
|
|
30639
|
+
{
|
|
30640
|
+
id: "C",
|
|
30641
|
+
label: "C: smooth",
|
|
30642
|
+
description: "B plus smoothing and a pressure curve.",
|
|
30643
|
+
profiles: {
|
|
30644
|
+
mouse: "stabilized",
|
|
30645
|
+
touch: "smooth",
|
|
30646
|
+
"pen.applePencil": "smooth",
|
|
30647
|
+
"pen.desktop": "smooth"
|
|
30648
|
+
},
|
|
30649
|
+
look: "widthCurve"
|
|
30650
|
+
},
|
|
30651
|
+
{
|
|
30652
|
+
id: "D",
|
|
30653
|
+
label: "D: round",
|
|
30654
|
+
description: "C plus the round outline.",
|
|
30655
|
+
profiles: {
|
|
30656
|
+
mouse: "stabilized",
|
|
30657
|
+
touch: "smooth",
|
|
30658
|
+
"pen.applePencil": "smooth",
|
|
30659
|
+
"pen.desktop": "smooth"
|
|
30660
|
+
},
|
|
30661
|
+
look: "round"
|
|
30662
|
+
},
|
|
30663
|
+
{
|
|
30664
|
+
id: "E",
|
|
30665
|
+
label: "E: freehand",
|
|
30666
|
+
description: "B plus the freehand outline.",
|
|
30667
|
+
profiles: {
|
|
30668
|
+
mouse: "fixed",
|
|
30669
|
+
touch: "fixed",
|
|
30670
|
+
"pen.applePencil": "fixed",
|
|
30671
|
+
"pen.desktop": "fixed"
|
|
30672
|
+
},
|
|
30673
|
+
look: "freehand"
|
|
30674
|
+
},
|
|
30675
|
+
{
|
|
30676
|
+
id: "F",
|
|
30677
|
+
label: "F: Illustrator-like",
|
|
30678
|
+
description: "Bézier-fitted finish, streamline stabilizing and a Size ± Variation width.",
|
|
30679
|
+
profiles: {
|
|
30680
|
+
mouse: "stabilized",
|
|
30681
|
+
touch: "smooth",
|
|
30682
|
+
"pen.applePencil": "illustrator",
|
|
30683
|
+
"pen.desktop": "illustrator"
|
|
30684
|
+
},
|
|
30685
|
+
look: "illustrator"
|
|
30686
|
+
}
|
|
30687
|
+
];
|
|
30688
|
+
/** Factory profile presets per profile. Read-only. */
|
|
30689
|
+
const BRUSH_PROFILE_PRESETS = deepFreeze(RAW_PROFILE_PRESETS);
|
|
30690
|
+
/** Factory look presets. Read-only. */
|
|
30691
|
+
const BRUSH_LOOK_PRESETS = deepFreeze(RAW_LOOK_PRESETS);
|
|
30692
|
+
/** Factory scenarios. Read-only. */
|
|
30693
|
+
const BRUSH_SCENARIOS = deepFreeze(RAW_SCENARIOS);
|
|
30694
|
+
/** Full profile configuration of a preset: the legacy profile with its patch. */
|
|
30695
|
+
function resolveBrushProfilePatch(patch) {
|
|
30696
|
+
return mergeData(BRUSH_LEGACY_INPUT_PROFILE, patch);
|
|
30697
|
+
}
|
|
30698
|
+
/** Full look of a preset, or `null` when the preset stores no look. */
|
|
30699
|
+
function resolveBrushLookPatch(patch) {
|
|
30700
|
+
return patch === null ? null : mergeData(WEAVE_STROKE_LEGACY_LOOK, patch);
|
|
30701
|
+
}
|
|
30702
|
+
function getBrushScenario(id) {
|
|
30703
|
+
return BRUSH_SCENARIOS.find((s) => s.id === id);
|
|
30704
|
+
}
|
|
30705
|
+
/**
|
|
30706
|
+
* Registry of profile and look presets: the read-only factory presets plus
|
|
30707
|
+
* custom ones. Factory presets cannot be changed or removed; a copy under a
|
|
30708
|
+
* new name can be made and edited. Custom presets live in memory; the
|
|
30709
|
+
* application decides whether to keep them.
|
|
30710
|
+
*/
|
|
30711
|
+
var WeaveBrushPresetRegistry = class WeaveBrushPresetRegistry {
|
|
30712
|
+
customProfiles = /* @__PURE__ */ new Map();
|
|
30713
|
+
customLooks = /* @__PURE__ */ new Map();
|
|
30714
|
+
static profileKey(key, id) {
|
|
30715
|
+
return `${key}/${id}`;
|
|
30716
|
+
}
|
|
30717
|
+
listProfilePresets(key) {
|
|
30718
|
+
const custom = [...this.customProfiles.entries()].filter(([k]) => k.startsWith(`${key}/`)).map(([, v]) => v);
|
|
30719
|
+
return [...BRUSH_PROFILE_PRESETS[key], ...custom];
|
|
30720
|
+
}
|
|
30721
|
+
getProfilePreset(key, id) {
|
|
30722
|
+
return this.listProfilePresets(key).find((p) => p.id === id);
|
|
30723
|
+
}
|
|
30724
|
+
isFactoryProfilePreset(key, id) {
|
|
30725
|
+
return BRUSH_PROFILE_PRESETS[key].some((p) => p.id === id);
|
|
30726
|
+
}
|
|
30727
|
+
/** Changes a custom profile preset; a factory preset is rejected. */
|
|
30728
|
+
setProfilePreset(key, id, patch) {
|
|
30729
|
+
if (this.isFactoryProfilePreset(key, id)) return {
|
|
30730
|
+
ok: false,
|
|
30731
|
+
error: `"${id}" is a factory preset and cannot be changed; copy it first`
|
|
30732
|
+
};
|
|
30733
|
+
const existing = this.customProfiles.get(WeaveBrushPresetRegistry.profileKey(key, id));
|
|
30734
|
+
if (!existing) return {
|
|
30735
|
+
ok: false,
|
|
30736
|
+
error: `Unknown preset "${id}"`
|
|
30737
|
+
};
|
|
30738
|
+
this.customProfiles.set(WeaveBrushPresetRegistry.profileKey(key, id), {
|
|
30739
|
+
...existing,
|
|
30740
|
+
patch: mergeData(existing.patch, patch)
|
|
30741
|
+
});
|
|
30742
|
+
return { ok: true };
|
|
30743
|
+
}
|
|
30744
|
+
/** Copies a preset under a new id. */
|
|
30745
|
+
copyProfilePreset(key, fromId, newId, label = newId) {
|
|
30746
|
+
const from = this.getProfilePreset(key, fromId);
|
|
30747
|
+
if (!from) return {
|
|
30748
|
+
ok: false,
|
|
30749
|
+
error: `Unknown preset "${fromId}"`
|
|
30750
|
+
};
|
|
30751
|
+
if (this.getProfilePreset(key, newId)) return {
|
|
30752
|
+
ok: false,
|
|
30753
|
+
error: `A preset "${newId}" already exists`
|
|
30754
|
+
};
|
|
30755
|
+
this.customProfiles.set(WeaveBrushPresetRegistry.profileKey(key, newId), {
|
|
30756
|
+
id: newId,
|
|
30757
|
+
label,
|
|
30758
|
+
description: `Copy of ${from.label}`,
|
|
30759
|
+
patch: mergeData(from.patch, {})
|
|
30760
|
+
});
|
|
30761
|
+
return { ok: true };
|
|
30762
|
+
}
|
|
30763
|
+
removeProfilePreset(key, id) {
|
|
30764
|
+
if (this.isFactoryProfilePreset(key, id)) return {
|
|
30765
|
+
ok: false,
|
|
30766
|
+
error: `"${id}" is a factory preset and cannot be removed`
|
|
30767
|
+
};
|
|
30768
|
+
return this.customProfiles.delete(WeaveBrushPresetRegistry.profileKey(key, id)) ? { ok: true } : {
|
|
30769
|
+
ok: false,
|
|
30770
|
+
error: `Unknown preset "${id}"`
|
|
30771
|
+
};
|
|
30772
|
+
}
|
|
30773
|
+
listLookPresets() {
|
|
30774
|
+
return [...BRUSH_LOOK_PRESETS, ...this.customLooks.values()];
|
|
30775
|
+
}
|
|
30776
|
+
getLookPreset(id) {
|
|
30777
|
+
return this.listLookPresets().find((p) => p.id === id);
|
|
30778
|
+
}
|
|
30779
|
+
isFactoryLookPreset(id) {
|
|
30780
|
+
return BRUSH_LOOK_PRESETS.some((p) => p.id === id);
|
|
30781
|
+
}
|
|
30782
|
+
/** Changes a custom look preset; a factory preset is rejected. */
|
|
30783
|
+
setLookPreset(id, patch) {
|
|
30784
|
+
if (this.isFactoryLookPreset(id)) return {
|
|
30785
|
+
ok: false,
|
|
30786
|
+
error: `"${id}" is a factory preset and cannot be changed; copy it first`
|
|
30787
|
+
};
|
|
30788
|
+
const existing = this.customLooks.get(id);
|
|
30789
|
+
if (!existing) return {
|
|
30790
|
+
ok: false,
|
|
30791
|
+
error: `Unknown preset "${id}"`
|
|
30792
|
+
};
|
|
30793
|
+
this.customLooks.set(id, {
|
|
30794
|
+
...existing,
|
|
30795
|
+
patch: mergeData(existing.patch ?? {}, patch)
|
|
30796
|
+
});
|
|
30797
|
+
return { ok: true };
|
|
30798
|
+
}
|
|
30799
|
+
copyLookPreset(fromId, newId, label = newId) {
|
|
30800
|
+
const from = this.getLookPreset(fromId);
|
|
30801
|
+
if (!from) return {
|
|
30802
|
+
ok: false,
|
|
30803
|
+
error: `Unknown preset "${fromId}"`
|
|
30804
|
+
};
|
|
30805
|
+
if (this.getLookPreset(newId)) return {
|
|
30806
|
+
ok: false,
|
|
30807
|
+
error: `A preset "${newId}" already exists`
|
|
30808
|
+
};
|
|
30809
|
+
this.customLooks.set(newId, {
|
|
30810
|
+
id: newId,
|
|
30811
|
+
label,
|
|
30812
|
+
description: `Copy of ${from.label}`,
|
|
30813
|
+
patch: from.patch === null ? null : mergeData(from.patch, {})
|
|
30814
|
+
});
|
|
30815
|
+
return { ok: true };
|
|
30816
|
+
}
|
|
30817
|
+
removeLookPreset(id) {
|
|
30818
|
+
if (this.isFactoryLookPreset(id)) return {
|
|
30819
|
+
ok: false,
|
|
30820
|
+
error: `"${id}" is a factory preset and cannot be removed`
|
|
30821
|
+
};
|
|
30822
|
+
return this.customLooks.delete(id) ? { ok: true } : {
|
|
30823
|
+
ok: false,
|
|
30824
|
+
error: `Unknown preset "${id}"`
|
|
30825
|
+
};
|
|
30826
|
+
}
|
|
30827
|
+
};
|
|
30828
|
+
//#endregion
|
|
30829
|
+
//#region src/actions/brush-tool/tunables/config-io.ts
|
|
30830
|
+
/** Version of the exported configuration document. */
|
|
30831
|
+
const BRUSH_CONFIG_DOCUMENT_VERSION = 1;
|
|
30832
|
+
const isObject$1 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
30833
|
+
/**
|
|
30834
|
+
* Reads and validates a configuration document, given as an object or as JSON
|
|
30835
|
+
* text. Nothing is applied: the result holds fully resolved values, or every
|
|
30836
|
+
* problem found.
|
|
30837
|
+
*/
|
|
30838
|
+
function parseBrushConfigDocument(input) {
|
|
30839
|
+
let doc = input;
|
|
30840
|
+
if (typeof input === "string") try {
|
|
30841
|
+
doc = JSON.parse(input);
|
|
30842
|
+
} catch {
|
|
30843
|
+
return {
|
|
30844
|
+
ok: false,
|
|
30845
|
+
errors: ["The document is not valid JSON"]
|
|
30846
|
+
};
|
|
30847
|
+
}
|
|
30848
|
+
if (!isObject$1(doc)) return {
|
|
30849
|
+
ok: false,
|
|
30850
|
+
errors: ["The document must be an object"]
|
|
30851
|
+
};
|
|
30852
|
+
if (doc["version"] !== 1) return {
|
|
30853
|
+
ok: false,
|
|
30854
|
+
errors: [`Unsupported document version "${String(doc["version"])}"; expected 1`]
|
|
30855
|
+
};
|
|
30856
|
+
const errors = [];
|
|
30857
|
+
const profiles = {};
|
|
30858
|
+
if (doc["profiles"] !== void 0) {
|
|
30859
|
+
if (!isObject$1(doc["profiles"])) errors.push("\"profiles\" must be an object");
|
|
30860
|
+
else for (const [key, value] of Object.entries(doc["profiles"])) {
|
|
30861
|
+
if (!BRUSH_INPUT_PROFILE_KEYS.includes(key)) {
|
|
30862
|
+
errors.push(`Unknown profile "${key}"`);
|
|
30863
|
+
continue;
|
|
30864
|
+
}
|
|
30865
|
+
const problems = validateBrushTunablePatch("profile", value);
|
|
30866
|
+
if (problems.length > 0) errors.push(...problems.map((p) => `${key}: ${p}`));
|
|
30867
|
+
else profiles[key] = resolveBrushProfilePatch(value);
|
|
30868
|
+
}
|
|
30869
|
+
}
|
|
30870
|
+
let look;
|
|
30871
|
+
if (doc["look"] !== void 0) {
|
|
30872
|
+
if (doc["look"] === null) look = null;
|
|
30873
|
+
else if (!isObject$1(doc["look"])) errors.push("\"look\" must be an object or null");
|
|
30874
|
+
else {
|
|
30875
|
+
const { version, ...settings } = doc["look"];
|
|
30876
|
+
if (version !== WEAVE_STROKE_LEGACY_LOOK.version) errors.push(`Unsupported look version "${String(version)}"`);
|
|
30877
|
+
else {
|
|
30878
|
+
const problems = validateBrushTunablePatch("look", settings);
|
|
30879
|
+
if (problems.length > 0) errors.push(...problems.map((p) => `look: ${p}`));
|
|
30880
|
+
else look = mergeData(WEAVE_STROKE_LEGACY_LOOK, settings);
|
|
30881
|
+
}
|
|
30882
|
+
}
|
|
30883
|
+
}
|
|
30884
|
+
return errors.length > 0 ? {
|
|
30885
|
+
ok: false,
|
|
30886
|
+
errors
|
|
30887
|
+
} : {
|
|
30888
|
+
ok: true,
|
|
30889
|
+
profiles,
|
|
30890
|
+
look
|
|
30891
|
+
};
|
|
30892
|
+
}
|
|
30893
|
+
//#endregion
|
|
30894
|
+
//#region src/actions/brush-tool/brush-tool.ts
|
|
30895
|
+
var WeaveBrushToolAction = class extends WeaveAction {
|
|
30896
|
+
config;
|
|
30897
|
+
initialized = false;
|
|
30898
|
+
state;
|
|
30899
|
+
clickPoint;
|
|
30900
|
+
strokeId;
|
|
30901
|
+
isSpacePressed = false;
|
|
30902
|
+
isEraser;
|
|
30903
|
+
container;
|
|
30904
|
+
measureContainer;
|
|
30905
|
+
cancelAction;
|
|
30906
|
+
prevTouchAction;
|
|
30907
|
+
penActive = false;
|
|
30908
|
+
lastSmoothedPressure = .5;
|
|
30909
|
+
hasPressureSample = false;
|
|
30910
|
+
lastPointerPos = null;
|
|
30911
|
+
lastPointerTime = 0;
|
|
30912
|
+
predictedCount = 0;
|
|
30913
|
+
activeProfile;
|
|
30914
|
+
activeProfileKey = "mouse";
|
|
30915
|
+
look = null;
|
|
30916
|
+
captureState = createCaptureState();
|
|
30917
|
+
pendingTouch = null;
|
|
30918
|
+
/** Preset each profile currently holds: a preset id, `custom` or `imported`. */
|
|
30919
|
+
presetIds = {
|
|
30920
|
+
mouse: "legacy",
|
|
30921
|
+
touch: "legacy",
|
|
30922
|
+
"pen.applePencil": "legacy",
|
|
30923
|
+
"pen.desktop": "legacy"
|
|
30924
|
+
};
|
|
30925
|
+
/** Scenario applied last, until a profile is changed on its own. */
|
|
30926
|
+
scenarioId = null;
|
|
30927
|
+
/** Pointer that is drawing the stroke in progress. */
|
|
30928
|
+
strokePointerId = null;
|
|
30929
|
+
/** A pen has been used since the tool was activated. */
|
|
30930
|
+
penSeen = false;
|
|
30931
|
+
/** Last position the pen reported, before any stabilizing. */
|
|
30932
|
+
lastRawPoint = null;
|
|
30933
|
+
stabilizer;
|
|
30934
|
+
pressureStateView;
|
|
30935
|
+
onPropsChange = void 0;
|
|
30936
|
+
onInit = void 0;
|
|
30937
|
+
constructor(params) {
|
|
30938
|
+
super();
|
|
30939
|
+
this.config = mergeExceptArrays(BRUSH_TOOL_DEFAULT_CONFIG, params?.config ?? {});
|
|
30940
|
+
for (const key of Object.keys(params?.config?.input?.profiles ?? {})) this.presetIds[key] = "custom";
|
|
30941
|
+
this.initialize();
|
|
30942
|
+
}
|
|
30943
|
+
initialize() {
|
|
30944
|
+
this.initialized = false;
|
|
30945
|
+
this.state = BRUSH_TOOL_STATE.INACTIVE;
|
|
30946
|
+
this.strokeId = null;
|
|
30947
|
+
this.clickPoint = null;
|
|
30948
|
+
this.container = void 0;
|
|
30949
|
+
this.isEraser = false;
|
|
30950
|
+
this.measureContainer = void 0;
|
|
30951
|
+
this.props = this.initProps();
|
|
30952
|
+
this.isSpacePressed = false;
|
|
30953
|
+
this.setActiveProfile("mouse");
|
|
30954
|
+
}
|
|
30955
|
+
setActiveProfile(key) {
|
|
30956
|
+
this.activeProfileKey = key;
|
|
30957
|
+
this.activeProfile = this.config.input.profiles[key];
|
|
30958
|
+
this.stabilizer = createStabilizer(this.activeProfile.stabilizer);
|
|
30959
|
+
}
|
|
30960
|
+
/** Key of the profile used by the stroke in progress, or the last one. */
|
|
30961
|
+
getActiveProfileKey() {
|
|
30962
|
+
return this.activeProfileKey;
|
|
30963
|
+
}
|
|
30964
|
+
/** Preset a profile currently holds: a preset id, `custom` or `imported`. */
|
|
30965
|
+
getPresetId(key) {
|
|
30966
|
+
return this.presetIds[key];
|
|
30967
|
+
}
|
|
30968
|
+
/** Scenario applied last, or `null` once a profile was changed on its own. */
|
|
30969
|
+
getScenarioId() {
|
|
30970
|
+
return this.scenarioId;
|
|
30971
|
+
}
|
|
30972
|
+
/**
|
|
30973
|
+
* How a stroke was drawn, recorded on it only while the lab plugin is
|
|
30974
|
+
* active, so ordinary documents carry no such data.
|
|
30975
|
+
*/
|
|
30976
|
+
getInputMeta() {
|
|
30977
|
+
const lab = this.instance.getPlugin(BRUSH_LAB_PLUGIN_NAME);
|
|
30978
|
+
if (!lab || lab.isEnabled?.() === false) return void 0;
|
|
30979
|
+
return {
|
|
30980
|
+
profile: this.activeProfileKey,
|
|
30981
|
+
preset: this.presetIds[this.activeProfileKey],
|
|
30982
|
+
...this.scenarioId ? { scenario: this.scenarioId } : {}
|
|
30983
|
+
};
|
|
30984
|
+
}
|
|
30985
|
+
/** The lab plugin, when it is registered and on; it only listens. */
|
|
30986
|
+
getTraceSink() {
|
|
30987
|
+
const lab = this.instance.getPlugin(BRUSH_LAB_PLUGIN_NAME);
|
|
30988
|
+
if (!lab || lab.isEnabled?.() === false) return null;
|
|
30989
|
+
return typeof lab.onSample === "function" ? lab : null;
|
|
30990
|
+
}
|
|
30991
|
+
/** Current configuration of a profile (a copy). */
|
|
30992
|
+
getInputProfile(key) {
|
|
30993
|
+
return mergeExceptArrays(this.config.input.profiles[key], {});
|
|
30994
|
+
}
|
|
30995
|
+
/**
|
|
30996
|
+
* Changes a profile's configuration. The change applies from the next
|
|
30997
|
+
* stroke: the profile object is replaced, never mutated, so a stroke in
|
|
30998
|
+
* progress keeps the configuration it started with.
|
|
30999
|
+
*/
|
|
31000
|
+
setInputProfile(key, patch) {
|
|
31001
|
+
if (!(key in this.config.input.profiles)) return {
|
|
31002
|
+
applied: false,
|
|
31003
|
+
errors: [`Unknown profile "${key}"`]
|
|
31004
|
+
};
|
|
31005
|
+
const errors = validateBrushTunablePatch("profile", patch);
|
|
31006
|
+
if (errors.length > 0) return {
|
|
31007
|
+
applied: false,
|
|
31008
|
+
errors
|
|
31009
|
+
};
|
|
31010
|
+
this.config.input.profiles[key] = mergeExceptArrays(this.config.input.profiles[key], patch);
|
|
31011
|
+
this.presetIds[key] = "custom";
|
|
31012
|
+
this.scenarioId = null;
|
|
31013
|
+
return {
|
|
31014
|
+
applied: true,
|
|
31015
|
+
errors: []
|
|
31016
|
+
};
|
|
31017
|
+
}
|
|
31018
|
+
/** The profiles and the look as a portable, versioned document. */
|
|
31019
|
+
exportConfig() {
|
|
31020
|
+
return {
|
|
31021
|
+
version: 1,
|
|
31022
|
+
profiles: {
|
|
31023
|
+
mouse: this.getInputProfile("mouse"),
|
|
31024
|
+
touch: this.getInputProfile("touch"),
|
|
31025
|
+
"pen.applePencil": this.getInputProfile("pen.applePencil"),
|
|
31026
|
+
"pen.desktop": this.getInputProfile("pen.desktop")
|
|
31027
|
+
},
|
|
31028
|
+
look: this.getLook()
|
|
31029
|
+
};
|
|
31030
|
+
}
|
|
31031
|
+
/** The configuration as JSON text. */
|
|
31032
|
+
exportConfigJson() {
|
|
31033
|
+
return JSON.stringify(this.exportConfig(), null, 2);
|
|
31034
|
+
}
|
|
31035
|
+
/**
|
|
31036
|
+
* Restores a configuration exported with `exportConfig`, given as an object
|
|
31037
|
+
* or JSON text. A document with an unsupported version or invalid values is
|
|
31038
|
+
* rejected as a whole and nothing changes.
|
|
31039
|
+
*/
|
|
31040
|
+
importConfig(input) {
|
|
31041
|
+
const parsed = parseBrushConfigDocument(input);
|
|
31042
|
+
if (!parsed.ok) return {
|
|
31043
|
+
applied: false,
|
|
31044
|
+
errors: parsed.errors
|
|
31045
|
+
};
|
|
31046
|
+
Object.assign(this.config.input.profiles, parsed.profiles);
|
|
31047
|
+
for (const key of Object.keys(parsed.profiles)) this.presetIds[key] = "imported";
|
|
31048
|
+
this.scenarioId = null;
|
|
31049
|
+
if (parsed.look !== void 0) this.look = parsed.look;
|
|
31050
|
+
return {
|
|
31051
|
+
applied: true,
|
|
31052
|
+
errors: []
|
|
31053
|
+
};
|
|
31054
|
+
}
|
|
31055
|
+
/** Look saved on every new stroke, or `null` when none is saved. */
|
|
31056
|
+
getLook() {
|
|
31057
|
+
return this.look ? mergeData(this.look, {}) : null;
|
|
31058
|
+
}
|
|
31059
|
+
/**
|
|
31060
|
+
* Changes the look saved on new strokes, or clears it with `null` so new
|
|
31061
|
+
* strokes carry no look and are drawn as in earlier releases. Strokes
|
|
31062
|
+
* already drawn keep the look they were saved with.
|
|
31063
|
+
*/
|
|
31064
|
+
setLook(patch) {
|
|
31065
|
+
if (patch === null) {
|
|
31066
|
+
this.look = null;
|
|
31067
|
+
return {
|
|
31068
|
+
applied: true,
|
|
31069
|
+
errors: []
|
|
31070
|
+
};
|
|
31071
|
+
}
|
|
31072
|
+
const errors = validateBrushTunablePatch("look", patch);
|
|
31073
|
+
if (errors.length > 0) return {
|
|
31074
|
+
applied: false,
|
|
31075
|
+
errors
|
|
31076
|
+
};
|
|
31077
|
+
this.look = mergeData(this.look ?? WEAVE_STROKE_LEGACY_LOOK, patch);
|
|
31078
|
+
return {
|
|
31079
|
+
applied: true,
|
|
31080
|
+
errors: []
|
|
31081
|
+
};
|
|
31082
|
+
}
|
|
31083
|
+
/**
|
|
31084
|
+
* Applies a scenario: one preset for each profile and one look preset.
|
|
31085
|
+
* Custom presets are looked up in `registry` when given.
|
|
31086
|
+
*/
|
|
31087
|
+
applyScenario(scenario, registry = new WeaveBrushPresetRegistry()) {
|
|
31088
|
+
const resolved = typeof scenario === "string" ? getBrushScenario(scenario) : scenario;
|
|
31089
|
+
if (!resolved) return {
|
|
31090
|
+
applied: false,
|
|
31091
|
+
errors: [`Unknown scenario "${scenario}"`]
|
|
31092
|
+
};
|
|
31093
|
+
const errors = [];
|
|
31094
|
+
const profiles = {};
|
|
31095
|
+
for (const key of Object.keys(this.config.input.profiles)) {
|
|
31096
|
+
const preset = registry.getProfilePreset(key, resolved.profiles[key]);
|
|
31097
|
+
if (!preset) {
|
|
31098
|
+
errors.push(`Unknown preset "${resolved.profiles[key]}" for profile "${key}"`);
|
|
31099
|
+
continue;
|
|
31100
|
+
}
|
|
31101
|
+
profiles[key] = resolveBrushProfilePatch(preset.patch);
|
|
31102
|
+
}
|
|
31103
|
+
const lookPreset = registry.getLookPreset(resolved.look);
|
|
31104
|
+
if (!lookPreset) errors.push(`Unknown look preset "${resolved.look}"`);
|
|
31105
|
+
if (errors.length > 0 || !lookPreset) return {
|
|
31106
|
+
applied: false,
|
|
31107
|
+
errors
|
|
31108
|
+
};
|
|
31109
|
+
Object.assign(this.config.input.profiles, profiles);
|
|
31110
|
+
this.presetIds = { ...resolved.profiles };
|
|
31111
|
+
this.scenarioId = resolved.id;
|
|
31112
|
+
this.look = resolveBrushLookPatch(lookPreset.patch);
|
|
31113
|
+
return {
|
|
31114
|
+
applied: true,
|
|
31115
|
+
errors: []
|
|
31116
|
+
};
|
|
31117
|
+
}
|
|
31118
|
+
/** Forces a profile for every stroke, or `null` to resolve automatically. */
|
|
31119
|
+
setForcedProfile(key) {
|
|
31120
|
+
this.config.input.forceProfile = key;
|
|
31121
|
+
}
|
|
31122
|
+
/** Replaces the automatic profile resolution; `undefined` restores it. */
|
|
31123
|
+
setProfileResolver(resolver) {
|
|
31124
|
+
this.config.input.resolveProfile = resolver;
|
|
31125
|
+
}
|
|
31126
|
+
/**
|
|
31127
|
+
* The pressure-smoothing state, as a view over this action's own fields so
|
|
31128
|
+
* the stage functions mutate the same state the action exposes.
|
|
31129
|
+
*/
|
|
31130
|
+
get pressureState() {
|
|
31131
|
+
if (!this.pressureStateView) {
|
|
31132
|
+
const self = this;
|
|
31133
|
+
this.pressureStateView = {
|
|
31134
|
+
get lastSmoothedPressure() {
|
|
31135
|
+
return self.lastSmoothedPressure;
|
|
31136
|
+
},
|
|
31137
|
+
set lastSmoothedPressure(v) {
|
|
31138
|
+
self.lastSmoothedPressure = v;
|
|
31139
|
+
},
|
|
31140
|
+
get hasPressureSample() {
|
|
31141
|
+
return self.hasPressureSample;
|
|
31142
|
+
},
|
|
31143
|
+
set hasPressureSample(v) {
|
|
31144
|
+
self.hasPressureSample = v;
|
|
31145
|
+
},
|
|
31146
|
+
get lastPointerPos() {
|
|
31147
|
+
return self.lastPointerPos;
|
|
31148
|
+
},
|
|
31149
|
+
set lastPointerPos(v) {
|
|
31150
|
+
self.lastPointerPos = v;
|
|
31151
|
+
},
|
|
31152
|
+
get lastPointerTime() {
|
|
31153
|
+
return self.lastPointerTime;
|
|
31154
|
+
},
|
|
31155
|
+
set lastPointerTime(v) {
|
|
31156
|
+
self.lastPointerTime = v;
|
|
31157
|
+
},
|
|
31158
|
+
get predictedCount() {
|
|
31159
|
+
return self.predictedCount;
|
|
31160
|
+
},
|
|
31161
|
+
set predictedCount(v) {
|
|
31162
|
+
self.predictedCount = v;
|
|
31163
|
+
},
|
|
31164
|
+
lastDerivative: 0,
|
|
31165
|
+
lastNonZeroPressure: null
|
|
31166
|
+
};
|
|
31167
|
+
}
|
|
31168
|
+
return this.pressureStateView;
|
|
31169
|
+
}
|
|
31170
|
+
getName() {
|
|
31171
|
+
return BRUSH_TOOL_ACTION_NAME;
|
|
31172
|
+
}
|
|
31173
|
+
initProps() {
|
|
31174
|
+
return {
|
|
31175
|
+
stroke: "#000000ff",
|
|
31176
|
+
strokeWidth: 1,
|
|
31177
|
+
opacity: 1
|
|
31178
|
+
};
|
|
31179
|
+
}
|
|
31180
|
+
/**
|
|
31181
|
+
* Smooths one raw pressure sample with the filter of the active profile.
|
|
31182
|
+
*
|
|
31183
|
+
* This is the one place pressure smoothing happens: every sample that
|
|
31184
|
+
* becomes a stroke point, including the high-frequency coalesced samples a
|
|
31185
|
+
* stylus reports, goes through it so the smoothing state advances for later
|
|
31186
|
+
* samples.
|
|
31187
|
+
*/
|
|
31188
|
+
smoothPressureSample(raw, clientX, clientY, time) {
|
|
31189
|
+
return smoothPressure(this.pressureState, this.activeProfile.pressure, raw, clientX, clientY, this.sampleTime(time));
|
|
31190
|
+
}
|
|
31191
|
+
/**
|
|
31192
|
+
* Time of a sample, in ms. With the `event` clock it is the time the sample's
|
|
31193
|
+
* own event reported, so processing a batch in one step does not change the
|
|
31194
|
+
* time between its samples; otherwise (or when the event reports no usable
|
|
31195
|
+
* time) it is the time of processing.
|
|
31196
|
+
*/
|
|
31197
|
+
sampleTime(eventTime) {
|
|
31198
|
+
if (this.activeProfile.capture.clock === "event" && typeof eventTime === "number" && Number.isFinite(eventTime)) return eventTime;
|
|
31199
|
+
return performance.now();
|
|
31200
|
+
}
|
|
31201
|
+
/**
|
|
28344
31202
|
* Raw pressure a pointer sample reports: the device's own reading for a
|
|
28345
31203
|
* pen, a neutral constant for devices that do not sense pressure.
|
|
28346
31204
|
*/
|
|
28347
|
-
getRawPressure(pointerType, pressure) {
|
|
28348
|
-
|
|
28349
|
-
return .5;
|
|
31205
|
+
getRawPressure(pointerType, pressure, commit = true) {
|
|
31206
|
+
return getRawPressure(pointerType, pressure, this.activeProfile.pressure.zero, this.pressureState, commit);
|
|
28350
31207
|
}
|
|
28351
31208
|
getEventPressure(e) {
|
|
28352
|
-
return this.smoothPressureSample(this.getRawPressure(e.evt.pointerType, e.evt.pressure), e.evt.clientX, e.evt.clientY);
|
|
31209
|
+
return this.smoothPressureSample(this.getRawPressure(e.evt.pointerType, e.evt.pressure), e.evt.clientX, e.evt.clientY, e.evt.timeStamp);
|
|
28353
31210
|
}
|
|
28354
31211
|
/**
|
|
28355
|
-
* Resets the
|
|
28356
|
-
*
|
|
28357
|
-
*
|
|
28358
|
-
* This state
|
|
28359
|
-
*
|
|
28360
|
-
*
|
|
28361
|
-
* otherwise that first sample can inherit
|
|
28362
|
-
*
|
|
28363
|
-
* inflating the very first point's pressure.
|
|
31212
|
+
* Resets the stroke-scoped state (pressure smoothing, predicted-point
|
|
31213
|
+
* bookkeeping and the position stabilizer) to a neutral baseline.
|
|
31214
|
+
*
|
|
31215
|
+
* This state belongs to a single stroke: it must be reset before the first
|
|
31216
|
+
* pressure sample of a new stroke is computed (in `handlePointerDown`, ahead
|
|
31217
|
+
* of the `getEventPressure` call for point 0), not only afterwards -
|
|
31218
|
+
* otherwise that first sample can inherit state left behind by the previous
|
|
31219
|
+
* stroke, inflating the very first point's pressure.
|
|
28364
31220
|
*/
|
|
28365
31221
|
resetPressureSmoothingState() {
|
|
28366
|
-
this.
|
|
28367
|
-
this.
|
|
28368
|
-
this.
|
|
28369
|
-
this.
|
|
28370
|
-
|
|
31222
|
+
resetPressureState(this.pressureState);
|
|
31223
|
+
this.captureState = createCaptureState();
|
|
31224
|
+
this.lastRawPoint = null;
|
|
31225
|
+
this.stabilizer.reset();
|
|
31226
|
+
}
|
|
31227
|
+
/**
|
|
31228
|
+
* Maps a client position to the stroke container the same way the stage maps
|
|
31229
|
+
* its own pointer position (content rectangle, then the inverse of the
|
|
31230
|
+
* container's absolute transform), without changing any stage state.
|
|
31231
|
+
*/
|
|
31232
|
+
clientToContainer(clientX, clientY) {
|
|
31233
|
+
const content = this.instance.getStage().getContent();
|
|
31234
|
+
const rect = content.getBoundingClientRect();
|
|
31235
|
+
const scaleX = rect.width / content.clientWidth || 1;
|
|
31236
|
+
const scaleY = rect.height / content.clientHeight || 1;
|
|
31237
|
+
const transform = this.measureContainer.getAbsoluteTransform().copy();
|
|
31238
|
+
transform.invert();
|
|
31239
|
+
return transform.point({
|
|
31240
|
+
x: (clientX - rect.left) / scaleX,
|
|
31241
|
+
y: (clientY - rect.top) / scaleY
|
|
31242
|
+
});
|
|
31243
|
+
}
|
|
31244
|
+
/** Captures the samples of one pointer move into the stroke in progress. */
|
|
31245
|
+
handleMoveEvent(e) {
|
|
31246
|
+
const deps = {
|
|
31247
|
+
smooth: (raw, clientX, clientY, time) => this.smoothPressureSample(raw, clientX, clientY, time),
|
|
31248
|
+
rawPressure: (pointerType, pressure, commit) => this.getRawPressure(pointerType, pressure, commit),
|
|
31249
|
+
predictedPressure: (raw) => peekPredictedPressure(this.pressureState, this.activeProfile.pressure, raw)
|
|
31250
|
+
};
|
|
31251
|
+
if (this.activeProfile.capture.source === "coalesced") this.applyMovementSamples(captureCoalescedSamples(e.evt, {
|
|
31252
|
+
...deps,
|
|
31253
|
+
toContainerPoint: (clientX, clientY) => this.clientToContainer(clientX, clientY)
|
|
31254
|
+
}, this.activeProfile.capture, this.captureState));
|
|
31255
|
+
else {
|
|
31256
|
+
const samples = captureLegacySamples(e.evt, deps);
|
|
31257
|
+
if (samples) this.applyMovementSamples(samples);
|
|
31258
|
+
else {
|
|
31259
|
+
const pointPressure = this.getEventPressure(e);
|
|
31260
|
+
this.handleMovement(pointPressure, void 0, false);
|
|
31261
|
+
}
|
|
31262
|
+
}
|
|
28371
31263
|
}
|
|
28372
31264
|
setupEvents() {
|
|
28373
31265
|
const stage = this.instance.getStage();
|
|
@@ -28394,52 +31286,65 @@ var WeaveBrushToolAction = class extends WeaveAction {
|
|
|
28394
31286
|
}, { signal: this.instance.getEventsController().signal });
|
|
28395
31287
|
const handlePointerDown = (e) => {
|
|
28396
31288
|
if (this.state === BRUSH_TOOL_STATE.INACTIVE) return;
|
|
31289
|
+
if (this.pendingTouch && e.evt.pointerType === "touch" && e.evt.pointerId !== this.pendingTouch.pointerId) {
|
|
31290
|
+
if (this.activeProfile.touchGuard.cancelOnSecondTouch) this.cancelPendingTouch();
|
|
31291
|
+
return;
|
|
31292
|
+
}
|
|
28397
31293
|
if (this.state !== BRUSH_TOOL_STATE.IDLE) return;
|
|
28398
31294
|
if (this.getZoomPlugin()?.isPinching()) return;
|
|
28399
31295
|
if (this.isSpacePressed) return;
|
|
28400
31296
|
if (e?.evt?.button !== 0) return;
|
|
28401
31297
|
if (e.evt.pointerType === "touch" && this.penActive) return;
|
|
28402
|
-
if (e.evt.pointerType === "pen")
|
|
31298
|
+
if (e.evt.pointerType === "pen") {
|
|
31299
|
+
this.penActive = true;
|
|
31300
|
+
this.penSeen = true;
|
|
31301
|
+
}
|
|
31302
|
+
this.setActiveProfile(resolveBrushInputProfileKey({
|
|
31303
|
+
pointerType: e.evt.pointerType,
|
|
31304
|
+
platform: detectBrushPlatform(),
|
|
31305
|
+
persistentDeviceId: e.evt.persistentDeviceId
|
|
31306
|
+
}, this.config.input));
|
|
31307
|
+
const guard = this.activeProfile.touchGuard;
|
|
31308
|
+
if (e.evt.pointerType === "touch" && guard.ignoreAfterPen && this.penSeen) return;
|
|
28403
31309
|
this.resetPressureSmoothingState();
|
|
28404
31310
|
const pointPressure = this.getEventPressure(e);
|
|
28405
|
-
this.
|
|
31311
|
+
this.strokePointerId = typeof e.evt.pointerId === "number" ? e.evt.pointerId : null;
|
|
31312
|
+
if (e.evt.pointerType === "touch" && (guard.waitMs > 0 || guard.movePx > 0)) {
|
|
31313
|
+
this.startPendingTouch(e, pointPressure);
|
|
31314
|
+
e.evt.stopPropagation();
|
|
31315
|
+
return;
|
|
31316
|
+
}
|
|
31317
|
+
this.handleStartStroke(pointPressure, e.evt.timeStamp);
|
|
31318
|
+
this.captureState.lastClient = {
|
|
31319
|
+
x: e.evt.clientX,
|
|
31320
|
+
y: e.evt.clientY
|
|
31321
|
+
};
|
|
28406
31322
|
e.evt.stopPropagation();
|
|
28407
31323
|
};
|
|
28408
31324
|
stage.on("pointerdown", handlePointerDown);
|
|
28409
31325
|
const handlePointerMove = (e) => {
|
|
28410
31326
|
if (this.state === BRUSH_TOOL_STATE.INACTIVE) return;
|
|
28411
31327
|
this.setCursor();
|
|
31328
|
+
if (this.pendingTouch && this.state === BRUSH_TOOL_STATE.IDLE) {
|
|
31329
|
+
this.handlePendingTouchMove(e);
|
|
31330
|
+
return;
|
|
31331
|
+
}
|
|
28412
31332
|
if (this.state !== BRUSH_TOOL_STATE.DEFINE_STROKE) return;
|
|
28413
31333
|
if (this.getZoomPlugin()?.isPinching()) return;
|
|
28414
|
-
|
|
28415
|
-
if (coalescedEvents.length > 1) {
|
|
28416
|
-
const samples = coalescedEvents.map((ce) => ({
|
|
28417
|
-
pressure: this.smoothPressureSample(this.getRawPressure(ce.pointerType, ce.pressure), ce.clientX, ce.clientY),
|
|
28418
|
-
isPredicted: false
|
|
28419
|
-
}));
|
|
28420
|
-
const predictedEvents = e.evt.getPredictedEvents ? e.evt.getPredictedEvents() : [];
|
|
28421
|
-
if (predictedEvents.length > 0) {
|
|
28422
|
-
const last = predictedEvents[predictedEvents.length - 1];
|
|
28423
|
-
const predPressure = Math.max(.15 * this.getRawPressure(last.pointerType, last.pressure) + .85 * this.lastSmoothedPressure, .15);
|
|
28424
|
-
samples.push({
|
|
28425
|
-
pressure: predPressure,
|
|
28426
|
-
sourceEvent: last,
|
|
28427
|
-
isPredicted: true
|
|
28428
|
-
});
|
|
28429
|
-
}
|
|
28430
|
-
this.applyMovementSamples(samples);
|
|
28431
|
-
} else {
|
|
28432
|
-
const pointPressure = this.getEventPressure(e);
|
|
28433
|
-
this.handleMovement(pointPressure, void 0, false);
|
|
28434
|
-
}
|
|
31334
|
+
this.handleMoveEvent(e);
|
|
28435
31335
|
e.evt.stopPropagation();
|
|
28436
31336
|
};
|
|
28437
31337
|
stage.on("pointermove", handlePointerMove);
|
|
28438
31338
|
const handlePointerUp = (e) => {
|
|
28439
31339
|
this.penActive = false;
|
|
31340
|
+
if (this.pendingTouch && this.state === BRUSH_TOOL_STATE.IDLE) {
|
|
31341
|
+
if (e.evt.pointerId !== this.pendingTouch.pointerId) return;
|
|
31342
|
+
this.commitPendingTouch();
|
|
31343
|
+
}
|
|
28440
31344
|
if (this.state !== BRUSH_TOOL_STATE.DEFINE_STROKE) return;
|
|
28441
|
-
if (this.
|
|
31345
|
+
if (this.strokePointerId !== null && typeof e.evt.pointerId === "number" && e.evt.pointerId !== this.strokePointerId) return;
|
|
28442
31346
|
this.handleEndStroke();
|
|
31347
|
+
this.strokePointerId = null;
|
|
28443
31348
|
e.evt.stopPropagation();
|
|
28444
31349
|
};
|
|
28445
31350
|
stage.on("pointerup", handlePointerUp);
|
|
@@ -28472,9 +31377,101 @@ var WeaveBrushToolAction = class extends WeaveAction {
|
|
|
28472
31377
|
height: maxY - minY
|
|
28473
31378
|
};
|
|
28474
31379
|
}
|
|
28475
|
-
|
|
31380
|
+
/** Puts a finger stroke on hold until it is clear it is not a gesture. */
|
|
31381
|
+
startPendingTouch(e, pressure) {
|
|
31382
|
+
const guard = this.activeProfile.touchGuard;
|
|
31383
|
+
const pending = {
|
|
31384
|
+
pointerId: e.evt.pointerId,
|
|
31385
|
+
startClient: {
|
|
31386
|
+
x: e.evt.clientX,
|
|
31387
|
+
y: e.evt.clientY
|
|
31388
|
+
},
|
|
31389
|
+
pressure,
|
|
31390
|
+
time: e.evt.timeStamp,
|
|
31391
|
+
start: this.instance.getMousePointer(),
|
|
31392
|
+
samples: [],
|
|
31393
|
+
timer: null
|
|
31394
|
+
};
|
|
31395
|
+
if (guard.waitMs > 0) pending.timer = setTimeout(() => this.commitPendingTouch(), guard.waitMs);
|
|
31396
|
+
this.pendingTouch = pending;
|
|
31397
|
+
}
|
|
31398
|
+
/** Drops a waiting finger stroke without leaving a mark. */
|
|
31399
|
+
cancelPendingTouch() {
|
|
31400
|
+
if (this.pendingTouch?.timer) clearTimeout(this.pendingTouch.timer);
|
|
31401
|
+
this.pendingTouch = null;
|
|
31402
|
+
this.strokePointerId = null;
|
|
31403
|
+
}
|
|
31404
|
+
/**
|
|
31405
|
+
* Starts the stroke of a waiting finger at the point where it touched down,
|
|
31406
|
+
* then replays the moves that were held back, so the path is not lost.
|
|
31407
|
+
*/
|
|
31408
|
+
commitPendingTouch() {
|
|
31409
|
+
const pending = this.pendingTouch;
|
|
31410
|
+
if (!pending) return;
|
|
31411
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
31412
|
+
this.pendingTouch = null;
|
|
31413
|
+
if (this.state !== BRUSH_TOOL_STATE.IDLE) return;
|
|
31414
|
+
this.handleStartStroke(pending.pressure, pending.time, pending.start);
|
|
31415
|
+
this.captureState.lastClient = pending.startClient;
|
|
31416
|
+
if (this.activeProfile.capture.source === "coalesced") for (const move of pending.samples) this.handleMoveEvent({ evt: {
|
|
31417
|
+
pointerId: move.pointerId,
|
|
31418
|
+
pointerType: move.pointerType,
|
|
31419
|
+
clientX: move.clientX,
|
|
31420
|
+
clientY: move.clientY,
|
|
31421
|
+
pressure: move.pressure,
|
|
31422
|
+
timeStamp: move.timeStamp,
|
|
31423
|
+
getCoalescedEvents: () => move.coalesced,
|
|
31424
|
+
getPredictedEvents: () => [],
|
|
31425
|
+
stopPropagation: () => void 0
|
|
31426
|
+
} });
|
|
31427
|
+
}
|
|
31428
|
+
/** Holds a move of the waiting finger, and commits once it has moved enough. */
|
|
31429
|
+
handlePendingTouchMove(e) {
|
|
31430
|
+
const pending = this.pendingTouch;
|
|
31431
|
+
if (!pending || e.evt.pointerId !== pending.pointerId) return;
|
|
31432
|
+
const guard = this.activeProfile.touchGuard;
|
|
31433
|
+
if (this.getZoomPlugin()?.isPinching()) {
|
|
31434
|
+
if (guard.cancelOnSecondTouch) this.cancelPendingTouch();
|
|
31435
|
+
return;
|
|
31436
|
+
}
|
|
31437
|
+
const batch = e.evt.getCoalescedEvents ? e.evt.getCoalescedEvents() : [];
|
|
31438
|
+
const source = batch.length > 0 ? batch : [e.evt];
|
|
31439
|
+
pending.samples.push({
|
|
31440
|
+
pointerId: e.evt.pointerId,
|
|
31441
|
+
pointerType: e.evt.pointerType,
|
|
31442
|
+
clientX: e.evt.clientX,
|
|
31443
|
+
clientY: e.evt.clientY,
|
|
31444
|
+
pressure: e.evt.pressure,
|
|
31445
|
+
timeStamp: e.evt.timeStamp,
|
|
31446
|
+
coalesced: source.map((c) => ({
|
|
31447
|
+
pointerType: c.pointerType,
|
|
31448
|
+
pressure: c.pressure,
|
|
31449
|
+
clientX: c.clientX,
|
|
31450
|
+
clientY: c.clientY,
|
|
31451
|
+
timeStamp: c.timeStamp
|
|
31452
|
+
}))
|
|
31453
|
+
});
|
|
31454
|
+
if (guard.movePx > 0 && Math.hypot(e.evt.clientX - pending.startClient.x, e.evt.clientY - pending.startClient.y) >= guard.movePx) this.commitPendingTouch();
|
|
31455
|
+
e.evt.stopPropagation();
|
|
31456
|
+
}
|
|
31457
|
+
handleStartStroke(pressure, time, start) {
|
|
28476
31458
|
this.resetPressureSmoothingState();
|
|
28477
|
-
const { mousePoint, container, measureContainer } = this.instance.getMousePointer();
|
|
31459
|
+
const { mousePoint, container, measureContainer } = start ?? this.instance.getMousePointer();
|
|
31460
|
+
if (mousePoint) {
|
|
31461
|
+
const self = this;
|
|
31462
|
+
this.stabilizer.seed(mousePoint, {
|
|
31463
|
+
get now() {
|
|
31464
|
+
return self.sampleTime(time);
|
|
31465
|
+
},
|
|
31466
|
+
get zoom() {
|
|
31467
|
+
return self.instance.getStage().scaleX();
|
|
31468
|
+
}
|
|
31469
|
+
});
|
|
31470
|
+
this.lastRawPoint = {
|
|
31471
|
+
x: mousePoint.x,
|
|
31472
|
+
y: mousePoint.y
|
|
31473
|
+
};
|
|
31474
|
+
}
|
|
28478
31475
|
this.clickPoint = mousePoint;
|
|
28479
31476
|
this.container = container;
|
|
28480
31477
|
this.measureContainer = measureContainer;
|
|
@@ -28487,8 +31484,11 @@ var WeaveBrushToolAction = class extends WeaveAction {
|
|
|
28487
31484
|
y: mousePoint.y,
|
|
28488
31485
|
pressure
|
|
28489
31486
|
});
|
|
31487
|
+
const inputMeta = this.getInputMeta();
|
|
28490
31488
|
const node = nodeHandler.create(this.strokeId, {
|
|
28491
31489
|
...this.props,
|
|
31490
|
+
...this.look ? { look: this.look } : {},
|
|
31491
|
+
...inputMeta ? { inputMeta } : {},
|
|
28492
31492
|
isEraser: this.isEraser,
|
|
28493
31493
|
strokeScaleEnabled: true,
|
|
28494
31494
|
x: 0,
|
|
@@ -28500,6 +31500,14 @@ var WeaveBrushToolAction = class extends WeaveAction {
|
|
|
28500
31500
|
const nodeInstance = nodeHandler.onRender(node.props);
|
|
28501
31501
|
this.measureContainer?.add(nodeInstance);
|
|
28502
31502
|
}
|
|
31503
|
+
this.getTraceSink()?.onStrokeStart({
|
|
31504
|
+
profile: this.activeProfileKey,
|
|
31505
|
+
preset: this.presetIds[this.activeProfileKey],
|
|
31506
|
+
...this.scenarioId ? { scenario: this.scenarioId } : {},
|
|
31507
|
+
profileConfig: this.activeProfile,
|
|
31508
|
+
strokeWidth: this.props.strokeWidth ?? 1,
|
|
31509
|
+
zoom: this.instance.getStage().scaleX()
|
|
31510
|
+
});
|
|
28503
31511
|
this.setState(BRUSH_TOOL_STATE.DEFINE_STROKE);
|
|
28504
31512
|
}
|
|
28505
31513
|
/**
|
|
@@ -28519,23 +31527,50 @@ var WeaveBrushToolAction = class extends WeaveAction {
|
|
|
28519
31527
|
const stage = this.instance.getStage();
|
|
28520
31528
|
const tempStroke = stage.findOne(`#${this.strokeId}`);
|
|
28521
31529
|
if (!this.measureContainer || !tempStroke) return;
|
|
31530
|
+
const sink = this.getTraceSink();
|
|
31531
|
+
const batchStart = sink ? performance.now() : 0;
|
|
28522
31532
|
let newStrokeElements = [...tempStroke.getAttrs().strokeElements];
|
|
31533
|
+
const skipPredicted = this.activeProfile.stabilizer.type !== "none";
|
|
28523
31534
|
for (const sample of samples) {
|
|
31535
|
+
if (sample.isPredicted && skipPredicted) continue;
|
|
28524
31536
|
if (sample.sourceEvent) stage.setPointersPositions(sample.sourceEvent);
|
|
28525
|
-
const
|
|
31537
|
+
const mousePoint = sample.point ?? this.instance.getMousePointerRelativeToContainer(this.measureContainer).mousePoint;
|
|
31538
|
+
if (!sample.isPredicted) this.lastRawPoint = {
|
|
31539
|
+
x: mousePoint.x,
|
|
31540
|
+
y: mousePoint.y
|
|
31541
|
+
};
|
|
31542
|
+
const sampleTime = () => this.sampleTime(sample.time);
|
|
31543
|
+
const stabilized = this.stabilizer.apply(mousePoint, {
|
|
31544
|
+
get now() {
|
|
31545
|
+
return sampleTime();
|
|
31546
|
+
},
|
|
31547
|
+
get zoom() {
|
|
31548
|
+
return stage.scaleX();
|
|
31549
|
+
}
|
|
31550
|
+
});
|
|
31551
|
+
sink?.onSample({
|
|
31552
|
+
raw: {
|
|
31553
|
+
x: mousePoint.x,
|
|
31554
|
+
y: mousePoint.y
|
|
31555
|
+
},
|
|
31556
|
+
stabilized: {
|
|
31557
|
+
x: stabilized.x,
|
|
31558
|
+
y: stabilized.y
|
|
31559
|
+
},
|
|
31560
|
+
pressure: sample.pressure,
|
|
31561
|
+
time: sampleTime(),
|
|
31562
|
+
predicted: sample.isPredicted
|
|
31563
|
+
});
|
|
28526
31564
|
const currentPoint = {
|
|
28527
|
-
x:
|
|
28528
|
-
y:
|
|
31565
|
+
x: stabilized.x - tempStroke.x(),
|
|
31566
|
+
y: stabilized.y - tempStroke.y(),
|
|
28529
31567
|
pressure: sample.pressure
|
|
28530
31568
|
};
|
|
28531
31569
|
if (!sample.isPredicted && this.predictedCount > 0) {
|
|
28532
31570
|
newStrokeElements = newStrokeElements.slice(0, -1 * this.predictedCount);
|
|
28533
31571
|
this.predictedCount = 0;
|
|
28534
31572
|
}
|
|
28535
|
-
|
|
28536
|
-
...newStrokeElements[0],
|
|
28537
|
-
pressure: currentPoint.pressure
|
|
28538
|
-
};
|
|
31573
|
+
applyStartPolicy(newStrokeElements, currentPoint, sample.isPredicted, this.activeProfile.pressure);
|
|
28539
31574
|
newStrokeElements.push(currentPoint);
|
|
28540
31575
|
if (sample.isPredicted) this.predictedCount++;
|
|
28541
31576
|
}
|
|
@@ -28549,6 +31584,10 @@ var WeaveBrushToolAction = class extends WeaveAction {
|
|
|
28549
31584
|
});
|
|
28550
31585
|
const nodeHandler = this.instance.getNodeHandler("stroke");
|
|
28551
31586
|
if (nodeHandler) nodeHandler.onUpdate(tempStroke, tempStroke.getAttrs());
|
|
31587
|
+
sink?.onBatch({
|
|
31588
|
+
ms: performance.now() - batchStart,
|
|
31589
|
+
samples: samples.length
|
|
31590
|
+
});
|
|
28552
31591
|
}
|
|
28553
31592
|
handleMovement(pressure, predictedEvent, isPredicted = false) {
|
|
28554
31593
|
this.applyMovementSamples([{
|
|
@@ -28557,26 +31596,60 @@ var WeaveBrushToolAction = class extends WeaveAction {
|
|
|
28557
31596
|
isPredicted
|
|
28558
31597
|
}]);
|
|
28559
31598
|
}
|
|
31599
|
+
/**
|
|
31600
|
+
* The point that completes a stabilized stroke at the pen's last position,
|
|
31601
|
+
* or `null` when the stroke already ends there or has no stabilizer.
|
|
31602
|
+
*/
|
|
31603
|
+
getStrokeCompletion(elements, tempStroke) {
|
|
31604
|
+
const cfg = this.activeProfile.stabilizer;
|
|
31605
|
+
if (cfg.type === "none" || !cfg.catchUpOnEnd) return null;
|
|
31606
|
+
if (!this.lastRawPoint || elements.length === 0) return null;
|
|
31607
|
+
const last = elements[elements.length - 1];
|
|
31608
|
+
const x = this.lastRawPoint.x - tempStroke.x();
|
|
31609
|
+
const y = this.lastRawPoint.y - tempStroke.y();
|
|
31610
|
+
if (x === last.x && y === last.y) return null;
|
|
31611
|
+
return {
|
|
31612
|
+
x,
|
|
31613
|
+
y,
|
|
31614
|
+
pressure: last.pressure
|
|
31615
|
+
};
|
|
31616
|
+
}
|
|
28560
31617
|
finalizeStroke(tempStroke, nodeHandler) {
|
|
28561
|
-
|
|
31618
|
+
let box = this.getBoundingBox(tempStroke.getAttrs().strokeElements);
|
|
28562
31619
|
let newStrokeElements = [...tempStroke.getAttrs().strokeElements];
|
|
28563
31620
|
if (this.predictedCount > 0) {
|
|
28564
31621
|
newStrokeElements = newStrokeElements.slice(0, -1 * this.predictedCount);
|
|
28565
31622
|
this.predictedCount = 0;
|
|
28566
31623
|
}
|
|
28567
|
-
|
|
28568
|
-
|
|
28569
|
-
newStrokeElements
|
|
28570
|
-
|
|
28571
|
-
pressure: newStrokeElements[lastIndex - 1].pressure
|
|
28572
|
-
};
|
|
31624
|
+
const completion = this.getStrokeCompletion(newStrokeElements, tempStroke);
|
|
31625
|
+
if (completion) {
|
|
31626
|
+
newStrokeElements = [...newStrokeElements, completion];
|
|
31627
|
+
box = this.getBoundingBox([...tempStroke.getAttrs().strokeElements, completion]);
|
|
28573
31628
|
}
|
|
31629
|
+
newStrokeElements = applyEndPolicy(newStrokeElements, this.activeProfile.pressure);
|
|
28574
31630
|
newStrokeElements = newStrokeElements.map((point) => ({
|
|
28575
31631
|
...point,
|
|
28576
31632
|
x: point.x - box.x,
|
|
28577
31633
|
y: point.y - box.y
|
|
28578
31634
|
}));
|
|
28579
|
-
const
|
|
31635
|
+
const finalize = this.activeProfile.finalize;
|
|
31636
|
+
const zoom = finalize.toleranceUnit === "screen" || finalize.mode === "bezierFit" || finalize.mode === "dpPressure" ? this.instance.getStage().scaleX() : 1;
|
|
31637
|
+
const finalizeStart = performance.now();
|
|
31638
|
+
const compressedPoints = finalizePoints(newStrokeElements, finalize, this.config.simplifyTolerance, zoom);
|
|
31639
|
+
this.getTraceSink()?.onStrokeEnd({
|
|
31640
|
+
beforeFinalize: newStrokeElements.map((p) => ({
|
|
31641
|
+
x: p.x + box.x,
|
|
31642
|
+
y: p.y + box.y,
|
|
31643
|
+
pressure: p.pressure
|
|
31644
|
+
})),
|
|
31645
|
+
finalPoints: compressedPoints.map((p) => ({
|
|
31646
|
+
x: p.x + box.x,
|
|
31647
|
+
y: p.y + box.y,
|
|
31648
|
+
pressure: p.pressure
|
|
31649
|
+
})),
|
|
31650
|
+
strokeWidth: tempStroke.getAttrs().strokeWidth ?? 1,
|
|
31651
|
+
finalizeMs: performance.now() - finalizeStart
|
|
31652
|
+
});
|
|
28580
31653
|
const sw = tempStroke.getAttrs().strokeWidth ?? 1;
|
|
28581
31654
|
const finalWidth = Math.max(box.width, sw);
|
|
28582
31655
|
const finalHeight = Math.max(box.height, sw);
|
|
@@ -28649,6 +31722,8 @@ var WeaveBrushToolAction = class extends WeaveAction {
|
|
|
28649
31722
|
stage.container().focus();
|
|
28650
31723
|
this.cancelAction = cancel;
|
|
28651
31724
|
if (selectionPlugin) selectionPlugin.setSelectedNodes([]);
|
|
31725
|
+
this.cancelPendingTouch();
|
|
31726
|
+
this.penSeen = false;
|
|
28652
31727
|
this.props = this.initProps();
|
|
28653
31728
|
this.setState(BRUSH_TOOL_STATE.IDLE);
|
|
28654
31729
|
this.instance.emitEvent("onAddingBrush");
|
|
@@ -28669,6 +31744,7 @@ var WeaveBrushToolAction = class extends WeaveAction {
|
|
|
28669
31744
|
if (node) selectionPlugin.setSelectedNodes([node]);
|
|
28670
31745
|
this.instance.triggerAction(SELECTION_TOOL_ACTION_NAME);
|
|
28671
31746
|
}
|
|
31747
|
+
this.cancelPendingTouch();
|
|
28672
31748
|
this.clickPoint = null;
|
|
28673
31749
|
this.setState(BRUSH_TOOL_STATE.INACTIVE);
|
|
28674
31750
|
}
|
|
@@ -37320,6 +40396,6 @@ var WeaveNodesSnappingPlugin = class extends WeavePlugin {
|
|
|
37320
40396
|
}
|
|
37321
40397
|
};
|
|
37322
40398
|
//#endregion
|
|
37323
|
-
export { ALIGN_NODES_ALIGN_TO, ALIGN_NODES_TOOL_ACTION_NAME, ALIGN_NODES_TOOL_STATE, BRUSH_TOOL_ACTION_NAME, BRUSH_TOOL_DEFAULT_CONFIG, BRUSH_TOOL_STATE, CONNECTOR_TOOL_ACTION_NAME, CONNECTOR_TOOL_DEFAULT_CONFIG, CONNECTOR_TOOL_STATE, COPY_PASTE_NODES_PLUGIN_STATE, DEFAULT_GUIDE_TOOL_ACTION_CONFIG, DEFAULT_SNAPPING_MANAGER_CONFIG, ELLIPSE_TOOL_ACTION_NAME, ELLIPSE_TOOL_STATE, ERASER_TOOL_ACTION_NAME, ERASER_TOOL_STATE, FRAME_TOOL_ACTION_NAME, FRAME_TOOL_STATE, GUIDE_DISTANCE_NAME, GUIDE_DISTANCE_ORIGIN, GUIDE_KIND, GUIDE_NAME, GUIDE_ORIENTATION, GUIDE_STATE, GUIDE_TOOL_ACTION_NAME, GUIDE_TOOL_STATE, LINE_TOOL_ACTION_NAME, LINE_TOOL_DEFAULT_CONFIG, LINE_TOOL_STATE, MEASURE_TOOL_ACTION_NAME, MEASURE_TOOL_STATE, MOVE_ORIENTATION, MOVE_TOOL_ACTION_NAME, MOVE_TOOL_STATE, PEN_TOOL_ACTION_NAME, PEN_TOOL_STATE, POLYGON_TOOL_ACTION_NAME, POLYGON_TOOL_STATE, PREVIEW_HANDOVER_MAX_FRAMES, PREVIEW_ID_SUFFIX, RECTANGLE_TOOL_ACTION_NAME, RECTANGLE_TOOL_STATE, REGULAR_POLYGON_TOOL_ACTION_NAME, REGULAR_POLYGON_TOOL_STATE, SELECTION_TOOL_ACTION_NAME, SELECTION_TOOL_STATE, STAGE_MINIMAP_DEFAULT_CONFIG, STAR_TOOL_ACTION_NAME, STAR_TOOL_STATE, TEXT_LAYOUT, TEXT_TOOL_ACTION_NAME, TEXT_TOOL_STATE, TextLayoutEngine, VIDEO_TOOL_ACTION_NAME, VIDEO_TOOL_STATE, WEAVE_ARROW_NODE_TYPE, WEAVE_ARROW_TOOL_ACTION_NAME, WEAVE_ARROW_TOOL_STATE, WEAVE_COMMENTS_RENDERER_KEY, WEAVE_COMMENTS_TOOL_LAYER_ID, WEAVE_COMMENT_CREATE_ACTION, WEAVE_COMMENT_NODE_ACTION, WEAVE_COMMENT_NODE_DEFAULTS, WEAVE_COMMENT_NODE_TYPE, WEAVE_COMMENT_STATUS, WEAVE_COMMENT_TOOL_ACTION_NAME, WEAVE_COMMENT_TOOL_DEFAULT_CONFIG, WEAVE_COMMENT_TOOL_STATE, WEAVE_COMMENT_VIEW_ACTION, WEAVE_CONNECTOR_NODE_ANCHOR_ORIGIN, WEAVE_CONNECTOR_NODE_DECORATOR_TYPE, WEAVE_CONNECTOR_NODE_DEFAULT_CONFIG, WEAVE_CONNECTOR_NODE_LINE_ORIGIN, WEAVE_CONNECTOR_NODE_LINE_TYPE, WEAVE_CONNECTOR_NODE_TYPE, WEAVE_COPY_PASTE_CONFIG_DEFAULT, WEAVE_COPY_PASTE_NODES_KEY, WEAVE_COPY_PASTE_PASTE_CATCHER_ID, WEAVE_COPY_PASTE_PASTE_MODES, WEAVE_DEFAULT_USER_INFO_FUNCTION, WEAVE_ELLIPSE_NODE_TYPE, WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR, WEAVE_FRAME_NODE_DEFAULT_CONFIG, WEAVE_FRAME_NODE_DEFAULT_PROPS, WEAVE_FRAME_NODE_TYPE, WEAVE_GRID_DEFAULT_CONFIG, WEAVE_GRID_DOT_TYPES, WEAVE_GRID_LAYER_ID, WEAVE_GRID_TYPES, WEAVE_GROUP_NODE_TYPE, WEAVE_IMAGES_TOOL_ACTION_NAME, WEAVE_IMAGES_TOOL_DEFAULT_CONFIG, WEAVE_IMAGES_TOOL_STATE, WEAVE_IMAGES_TOOL_UPLOAD_TYPE, WEAVE_IMAGE_CROP_ANCHOR_POSITION, WEAVE_IMAGE_CROP_END_TYPE, WEAVE_IMAGE_DEFAULT_CONFIG, WEAVE_IMAGE_NODE_TYPE, WEAVE_IMAGE_TOOL_ACTION_NAME, WEAVE_IMAGE_TOOL_CONFIG_DEFAULT, WEAVE_IMAGE_TOOL_STATE, WEAVE_IMAGE_TOOL_UPLOAD_TYPE, WEAVE_LAYER_NODE_TYPE, WEAVE_LINE_NODE_DEFAULT_CONFIG, WEAVE_LINE_NODE_TYPE, WEAVE_MEASURE_NODE_DEFAULT_CONFIG, WEAVE_MEASURE_NODE_TYPE, WEAVE_MEASURE_TOOL_DEFAULT_CONFIG, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_DEFAULT_CONFIG, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_KEY, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_LAYER_ID, WEAVE_NODES_SELECTION_DEFAULT_CONFIG, WEAVE_NODES_SELECTION_KEY, WEAVE_NODES_SELECTION_LAYER_ID, WEAVE_NODES_SNAPPING_PLUGIN_KEY, WEAVE_POLYGON_NODE_TYPE, WEAVE_POLYGON_PRESETS, WEAVE_RECTANGLE_NODE_TYPE, WEAVE_REGULAR_POLYGON_NODE_TYPE, WEAVE_SHAPE_LABEL_DEFAULTS, WEAVE_STAGE_DEFAULT_MODE, WEAVE_STAGE_DROP_AREA_KEY, WEAVE_STAGE_GRID_PLUGIN_KEY, WEAVE_STAGE_IMAGE_CROPPING_MODE, WEAVE_STAGE_KEYBOARD_MOVE_DEFAULT_CONFIG, WEAVE_STAGE_KEYBOARD_MOVE_KEY, WEAVE_STAGE_KEYBOARD_MOVE_ORIENTATION, WEAVE_STAGE_MINIMAP_KEY, WEAVE_STAGE_NODE_TYPE, WEAVE_STAGE_PANNING_DEFAULT_CONFIG, WEAVE_STAGE_PANNING_KEY, WEAVE_STAGE_PANNING_THROTTLE_MS, WEAVE_STAGE_SHAPE_LABEL_EDITION_MODE, WEAVE_STAGE_TEXT_EDITION_MODE, WEAVE_STAGE_ZOOM_DEFAULT_CONFIG, WEAVE_STAGE_ZOOM_KEY, WEAVE_STAGE_ZOOM_TYPE, WEAVE_STAR_NODE_TYPE, WEAVE_STROKE_NODE_DEFAULT_CONFIG, WEAVE_STROKE_NODE_TYPE, WEAVE_STROKE_SINGLE_NODE_DEFAULT_CONFIG, WEAVE_STROKE_SINGLE_NODE_TIP_SIDE, WEAVE_STROKE_SINGLE_NODE_TIP_TYPE, WEAVE_STROKE_SINGLE_NODE_TYPE, WEAVE_STROKE_TOOL_ACTION_NAME, WEAVE_STROKE_TOOL_ACTION_NAME_ALIASES, WEAVE_STROKE_TOOL_DEFAULT_CONFIG, WEAVE_STROKE_TOOL_STATE, WEAVE_TEXT_LINK_ALLOWED_PROTOCOLS, WEAVE_TEXT_NODE_DEFAULT_CONFIG, WEAVE_TEXT_NODE_TYPE, WEAVE_USERS_POINTERS_CONFIG_DEFAULT_PROPS, WEAVE_USERS_POINTERS_KEY, WEAVE_USERS_PRESENCE_CONFIG_DEFAULT_PROPS, WEAVE_USERS_PRESENCE_PLUGIN_KEY, WEAVE_USERS_SELECTION_KEY, WEAVE_USER_POINTER_KEY, WEAVE_USER_PRESENCE_KEY, WEAVE_USER_SELECTION_KEY, WEAVE_VIDEO_DEFAULT_CONFIG, WEAVE_VIDEO_NODE_TYPE, Weave, WeaveAction, WeaveAlignNodesToolAction, WeaveArrowNode, WeaveArrowToolAction, WeaveBrushToolAction, WeaveCommentNode, WeaveCommentToolAction, WeaveCommentsRendererPlugin, WeaveConnectedUsersPlugin, WeaveConnectorNode, WeaveConnectorToolAction, WeaveContextMenuPlugin, WeaveCopyPasteNodesPlugin, WeaveEllipseNode, WeaveEllipseToolAction, WeaveEraserToolAction, WeaveExportNodesToolAction, WeaveExportStageToolAction, WeaveFitToScreenToolAction, WeaveFitToSelectionToolAction, WeaveFrameNode, WeaveFrameToolAction, WeaveGroupNode, WeaveGuideToolAction, WeaveImageNode, WeaveImageToolAction, WeaveImagesToolAction, WeaveLayerNode, WeaveLineNode, WeaveLineToolAction, WeaveMeasureNode, WeaveMeasureToolAction, WeaveMoveToolAction, WeaveNode, WeaveNodesMultiSelectionFeedbackPlugin, WeaveNodesSelectionPlugin, WeaveNodesSnappingPlugin, WeavePenToolAction, WeavePlugin, WeavePolygonNode, WeavePolygonToolAction, WeaveRectangleNode, WeaveRectangleToolAction, WeaveRegularPolygonNode, WeaveRegularPolygonToolAction, WeaveRenderer, WeaveSelectionToolAction, WeaveStageDropAreaPlugin, WeaveStageGridPlugin, WeaveStageKeyboardMovePlugin, WeaveStageMinimapPlugin, WeaveStageNode, WeaveStagePanningPlugin, WeaveStageResizePlugin, WeaveStageZoomPlugin, WeaveStarNode, WeaveStarToolAction, WeaveStateManipulation, WeaveStore, WeaveStrokeNode, WeaveStrokeSingleNode, WeaveStrokeToolAction, WeaveTextNode, WeaveTextToolAction, WeaveTextV2Node, WeaveTextV2Shape, WeaveUsersPointersPlugin, WeaveUsersPresencePlugin, WeaveUsersSelectionPlugin, WeaveVideoNode, WeaveVideoToolAction, WeaveZoomInToolAction, WeaveZoomOutToolAction, applyLabelBlocksState, buildAncestorGroupIds, canComposite, clearContainerTargets, computeEllipseLabelMinSize, computePolygonLabelMinSize, computeRectangleLabelMinSize, containerOverCursor, containsNodeDeep, defaultInitialState, downscaleImageFile, downscaleImageFromURL, getBoundingBox, getDownscaleRatio, getExportBoundingBox, getImageSizeFromFile, getJSONFromYjsBinary, getSelectedNodesMetadata, getShapeLabelSchemaFields, getStageClickPoint, getTargetAndSkipNodes, getTargetedNode, getTopmostShadowHost, getVisibleNodes, getVisibleNodesInViewport, hasFrames, hasImages, instantiatePreset, intersectArrays, isArray, isIOS, isInShadowDOM, isNodeInSelection, isNumber, isObject, isServer, labelId, loadImageSource, mapJsonToYjsArray, mapJsonToYjsElements, mapJsonToYjsMap, memoize, mergeExceptArrays, moveNodeToContainer, moveNodeToContainerNT, resetScale, resolveLabelBlocks, setupCanvasBackend, setupSkiaBackend, sleep, spreadLabelProps, weavejsToYjsBinary };
|
|
40399
|
+
export { ALIGN_NODES_ALIGN_TO, ALIGN_NODES_TOOL_ACTION_NAME, ALIGN_NODES_TOOL_STATE, BRUSH_CONFIG_DOCUMENT_VERSION, BRUSH_INPUT_PROFILE_KEYS, BRUSH_LAB_PLUGIN_NAME, BRUSH_LEGACY_INPUT_PROFILE, BRUSH_LOOK_PRESETS, BRUSH_LOOK_TUNABLES, BRUSH_PROFILE_PRESETS, BRUSH_PROFILE_TUNABLES, BRUSH_SCENARIOS, BRUSH_TOOL_ACTION_NAME, BRUSH_TOOL_DEFAULT_CONFIG, BRUSH_TOOL_STATE, BRUSH_TUNABLES, CONNECTOR_TOOL_ACTION_NAME, CONNECTOR_TOOL_DEFAULT_CONFIG, CONNECTOR_TOOL_STATE, COPY_PASTE_NODES_PLUGIN_STATE, DEFAULT_GUIDE_TOOL_ACTION_CONFIG, DEFAULT_SNAPPING_MANAGER_CONFIG, ELLIPSE_TOOL_ACTION_NAME, ELLIPSE_TOOL_STATE, ERASER_TOOL_ACTION_NAME, ERASER_TOOL_STATE, FRAME_TOOL_ACTION_NAME, FRAME_TOOL_STATE, GUIDE_DISTANCE_NAME, GUIDE_DISTANCE_ORIGIN, GUIDE_KIND, GUIDE_NAME, GUIDE_ORIENTATION, GUIDE_STATE, GUIDE_TOOL_ACTION_NAME, GUIDE_TOOL_STATE, LINE_TOOL_ACTION_NAME, LINE_TOOL_DEFAULT_CONFIG, LINE_TOOL_STATE, MEASURE_TOOL_ACTION_NAME, MEASURE_TOOL_STATE, MOVE_ORIENTATION, MOVE_TOOL_ACTION_NAME, MOVE_TOOL_STATE, PEN_TOOL_ACTION_NAME, PEN_TOOL_STATE, POLYGON_TOOL_ACTION_NAME, POLYGON_TOOL_STATE, PREVIEW_HANDOVER_MAX_FRAMES, PREVIEW_ID_SUFFIX, RECTANGLE_TOOL_ACTION_NAME, RECTANGLE_TOOL_STATE, REGULAR_POLYGON_TOOL_ACTION_NAME, REGULAR_POLYGON_TOOL_STATE, SELECTION_TOOL_ACTION_NAME, SELECTION_TOOL_STATE, STAGE_MINIMAP_DEFAULT_CONFIG, STAR_TOOL_ACTION_NAME, STAR_TOOL_STATE, TEXT_LAYOUT, TEXT_TOOL_ACTION_NAME, TEXT_TOOL_STATE, TextLayoutEngine, VIDEO_TOOL_ACTION_NAME, VIDEO_TOOL_STATE, WEAVE_ARROW_NODE_TYPE, WEAVE_ARROW_TOOL_ACTION_NAME, WEAVE_ARROW_TOOL_STATE, WEAVE_COMMENTS_RENDERER_KEY, WEAVE_COMMENTS_TOOL_LAYER_ID, WEAVE_COMMENT_CREATE_ACTION, WEAVE_COMMENT_NODE_ACTION, WEAVE_COMMENT_NODE_DEFAULTS, WEAVE_COMMENT_NODE_TYPE, WEAVE_COMMENT_STATUS, WEAVE_COMMENT_TOOL_ACTION_NAME, WEAVE_COMMENT_TOOL_DEFAULT_CONFIG, WEAVE_COMMENT_TOOL_STATE, WEAVE_COMMENT_VIEW_ACTION, WEAVE_CONNECTOR_NODE_ANCHOR_ORIGIN, WEAVE_CONNECTOR_NODE_DECORATOR_TYPE, WEAVE_CONNECTOR_NODE_DEFAULT_CONFIG, WEAVE_CONNECTOR_NODE_LINE_ORIGIN, WEAVE_CONNECTOR_NODE_LINE_TYPE, WEAVE_CONNECTOR_NODE_TYPE, WEAVE_COPY_PASTE_CONFIG_DEFAULT, WEAVE_COPY_PASTE_NODES_KEY, WEAVE_COPY_PASTE_PASTE_CATCHER_ID, WEAVE_COPY_PASTE_PASTE_MODES, WEAVE_DEFAULT_USER_INFO_FUNCTION, WEAVE_ELLIPSE_NODE_TYPE, WEAVE_FRAME_DEFAULT_BACKGROUND_COLOR, WEAVE_FRAME_NODE_DEFAULT_CONFIG, WEAVE_FRAME_NODE_DEFAULT_PROPS, WEAVE_FRAME_NODE_TYPE, WEAVE_GRID_DEFAULT_CONFIG, WEAVE_GRID_DOT_TYPES, WEAVE_GRID_LAYER_ID, WEAVE_GRID_TYPES, WEAVE_GROUP_NODE_TYPE, WEAVE_IMAGES_TOOL_ACTION_NAME, WEAVE_IMAGES_TOOL_DEFAULT_CONFIG, WEAVE_IMAGES_TOOL_STATE, WEAVE_IMAGES_TOOL_UPLOAD_TYPE, WEAVE_IMAGE_CROP_ANCHOR_POSITION, WEAVE_IMAGE_CROP_END_TYPE, WEAVE_IMAGE_DEFAULT_CONFIG, WEAVE_IMAGE_NODE_TYPE, WEAVE_IMAGE_TOOL_ACTION_NAME, WEAVE_IMAGE_TOOL_CONFIG_DEFAULT, WEAVE_IMAGE_TOOL_STATE, WEAVE_IMAGE_TOOL_UPLOAD_TYPE, WEAVE_LAYER_NODE_TYPE, WEAVE_LINE_NODE_DEFAULT_CONFIG, WEAVE_LINE_NODE_TYPE, WEAVE_MEASURE_NODE_DEFAULT_CONFIG, WEAVE_MEASURE_NODE_TYPE, WEAVE_MEASURE_TOOL_DEFAULT_CONFIG, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_DEFAULT_CONFIG, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_KEY, WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_LAYER_ID, WEAVE_NODES_SELECTION_DEFAULT_CONFIG, WEAVE_NODES_SELECTION_KEY, WEAVE_NODES_SELECTION_LAYER_ID, WEAVE_NODES_SNAPPING_PLUGIN_KEY, WEAVE_POLYGON_NODE_TYPE, WEAVE_POLYGON_PRESETS, WEAVE_RECTANGLE_NODE_TYPE, WEAVE_REGULAR_POLYGON_NODE_TYPE, WEAVE_SHAPE_LABEL_DEFAULTS, WEAVE_STAGE_DEFAULT_MODE, WEAVE_STAGE_DROP_AREA_KEY, WEAVE_STAGE_GRID_PLUGIN_KEY, WEAVE_STAGE_IMAGE_CROPPING_MODE, WEAVE_STAGE_KEYBOARD_MOVE_DEFAULT_CONFIG, WEAVE_STAGE_KEYBOARD_MOVE_KEY, WEAVE_STAGE_KEYBOARD_MOVE_ORIENTATION, WEAVE_STAGE_MINIMAP_KEY, WEAVE_STAGE_NODE_TYPE, WEAVE_STAGE_PANNING_DEFAULT_CONFIG, WEAVE_STAGE_PANNING_KEY, WEAVE_STAGE_PANNING_THROTTLE_MS, WEAVE_STAGE_SHAPE_LABEL_EDITION_MODE, WEAVE_STAGE_TEXT_EDITION_MODE, WEAVE_STAGE_ZOOM_DEFAULT_CONFIG, WEAVE_STAGE_ZOOM_KEY, WEAVE_STAGE_ZOOM_TYPE, WEAVE_STAR_NODE_TYPE, WEAVE_STROKE_LEGACY_LOOK, WEAVE_STROKE_NODE_DEFAULT_CONFIG, WEAVE_STROKE_NODE_TYPE, WEAVE_STROKE_SINGLE_NODE_DEFAULT_CONFIG, WEAVE_STROKE_SINGLE_NODE_TIP_SIDE, WEAVE_STROKE_SINGLE_NODE_TIP_TYPE, WEAVE_STROKE_SINGLE_NODE_TYPE, WEAVE_STROKE_TOOL_ACTION_NAME, WEAVE_STROKE_TOOL_ACTION_NAME_ALIASES, WEAVE_STROKE_TOOL_DEFAULT_CONFIG, WEAVE_STROKE_TOOL_STATE, WEAVE_TEXT_LINK_ALLOWED_PROTOCOLS, WEAVE_TEXT_NODE_DEFAULT_CONFIG, WEAVE_TEXT_NODE_TYPE, WEAVE_USERS_POINTERS_CONFIG_DEFAULT_PROPS, WEAVE_USERS_POINTERS_KEY, WEAVE_USERS_PRESENCE_CONFIG_DEFAULT_PROPS, WEAVE_USERS_PRESENCE_PLUGIN_KEY, WEAVE_USERS_SELECTION_KEY, WEAVE_USER_POINTER_KEY, WEAVE_USER_PRESENCE_KEY, WEAVE_USER_SELECTION_KEY, WEAVE_VIDEO_DEFAULT_CONFIG, WEAVE_VIDEO_NODE_TYPE, Weave, WeaveAction, WeaveAlignNodesToolAction, WeaveArrowNode, WeaveArrowToolAction, WeaveBrushPresetRegistry, WeaveBrushToolAction, WeaveCommentNode, WeaveCommentToolAction, WeaveCommentsRendererPlugin, WeaveConnectedUsersPlugin, WeaveConnectorNode, WeaveConnectorToolAction, WeaveContextMenuPlugin, WeaveCopyPasteNodesPlugin, WeaveEllipseNode, WeaveEllipseToolAction, WeaveEraserToolAction, WeaveExportNodesToolAction, WeaveExportStageToolAction, WeaveFitToScreenToolAction, WeaveFitToSelectionToolAction, WeaveFrameNode, WeaveFrameToolAction, WeaveGroupNode, WeaveGuideToolAction, WeaveImageNode, WeaveImageToolAction, WeaveImagesToolAction, WeaveLayerNode, WeaveLineNode, WeaveLineToolAction, WeaveMeasureNode, WeaveMeasureToolAction, WeaveMoveToolAction, WeaveNode, WeaveNodesMultiSelectionFeedbackPlugin, WeaveNodesSelectionPlugin, WeaveNodesSnappingPlugin, WeavePenToolAction, WeavePlugin, WeavePolygonNode, WeavePolygonToolAction, WeaveRectangleNode, WeaveRectangleToolAction, WeaveRegularPolygonNode, WeaveRegularPolygonToolAction, WeaveRenderer, WeaveSelectionToolAction, WeaveStageDropAreaPlugin, WeaveStageGridPlugin, WeaveStageKeyboardMovePlugin, WeaveStageMinimapPlugin, WeaveStageNode, WeaveStagePanningPlugin, WeaveStageResizePlugin, WeaveStageZoomPlugin, WeaveStarNode, WeaveStarToolAction, WeaveStateManipulation, WeaveStore, WeaveStrokeNode, WeaveStrokeSingleNode, WeaveStrokeToolAction, WeaveTextNode, WeaveTextToolAction, WeaveTextV2Node, WeaveTextV2Shape, WeaveUsersPointersPlugin, WeaveUsersPresencePlugin, WeaveUsersSelectionPlugin, WeaveVideoNode, WeaveVideoToolAction, WeaveZoomInToolAction, WeaveZoomOutToolAction, applyLabelBlocksState, automaticBrushProfileKey, buildAncestorGroupIds, canComposite, clearContainerTargets, computeEllipseLabelMinSize, computePolygonLabelMinSize, computeRectangleLabelMinSize, containerOverCursor, containsNodeDeep, defaultInitialState, detectBrushPlatform, downscaleImageFile, downscaleImageFromURL, getBoundingBox, getBrushScenario, getBrushTunable, getDownscaleRatio, getExportBoundingBox, getImageSizeFromFile, getJSONFromYjsBinary, getSelectedNodesMetadata, getShapeLabelSchemaFields, getStageClickPoint, getTargetAndSkipNodes, getTargetedNode, getTopmostShadowHost, getVisibleNodes, getVisibleNodesInViewport, hasFrames, hasImages, instantiatePreset, intersectArrays, isArray, isIOS, isInShadowDOM, isNodeInSelection, isNumber, isObject, isServer, labelId, loadImageSource, mapJsonToYjsArray, mapJsonToYjsElements, mapJsonToYjsMap, memoize, mergeExceptArrays, moveNodeToContainer, moveNodeToContainerNT, parseBrushConfigDocument, resetScale, resolveBrushInputProfileKey, resolveBrushLookPatch, resolveBrushProfilePatch, resolveLabelBlocks, setupCanvasBackend, setupSkiaBackend, sleep, spreadLabelProps, validateBrushTunablePatch, validateBrushTunableValue, weavejsToYjsBinary };
|
|
37324
40400
|
|
|
37325
40401
|
//# sourceMappingURL=types.js.map
|