@oh-just-another/state 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +2 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/actions/actionArrange.js +4 -4
  5. package/dist/actions/actionArrange.js.map +1 -1
  6. package/dist/actions/actionClipboard.d.ts.map +1 -1
  7. package/dist/actions/actionClipboard.js +5 -0
  8. package/dist/actions/actionClipboard.js.map +1 -1
  9. package/dist/actions/actionKeyboard.d.ts.map +1 -1
  10. package/dist/actions/actionKeyboard.js +77 -9
  11. package/dist/actions/actionKeyboard.js.map +1 -1
  12. package/dist/actions/actionMode.d.ts +4 -0
  13. package/dist/actions/actionMode.d.ts.map +1 -1
  14. package/dist/actions/actionMode.js +54 -0
  15. package/dist/actions/actionMode.js.map +1 -1
  16. package/dist/actions/actionSelection.d.ts.map +1 -1
  17. package/dist/actions/actionSelection.js +1 -0
  18. package/dist/actions/actionSelection.js.map +1 -1
  19. package/dist/actions/actionView.d.ts +6 -0
  20. package/dist/actions/actionView.d.ts.map +1 -1
  21. package/dist/actions/actionView.js +19 -1
  22. package/dist/actions/actionView.js.map +1 -1
  23. package/dist/actions/actionZoom.d.ts.map +1 -1
  24. package/dist/actions/actionZoom.js +5 -0
  25. package/dist/actions/actionZoom.js.map +1 -1
  26. package/dist/actions/index.d.ts +3 -3
  27. package/dist/actions/index.d.ts.map +1 -1
  28. package/dist/actions/index.js +3 -3
  29. package/dist/actions/index.js.map +1 -1
  30. package/dist/actions/registry.d.ts.map +1 -1
  31. package/dist/actions/registry.js +13 -0
  32. package/dist/actions/registry.js.map +1 -1
  33. package/dist/actions/types.d.ts +9 -0
  34. package/dist/actions/types.d.ts.map +1 -1
  35. package/dist/actions/types.js.map +1 -1
  36. package/dist/clipboard.d.ts +0 -14
  37. package/dist/clipboard.d.ts.map +1 -1
  38. package/dist/clipboard.js +1 -1
  39. package/dist/clipboard.js.map +1 -1
  40. package/dist/constants.d.ts +160 -18
  41. package/dist/constants.d.ts.map +1 -1
  42. package/dist/constants.js +162 -2
  43. package/dist/constants.js.map +1 -1
  44. package/dist/dom-events.d.ts +1 -2
  45. package/dist/dom-events.d.ts.map +1 -1
  46. package/dist/dom-events.js.map +1 -1
  47. package/dist/editor/animation-scene.d.ts +15 -0
  48. package/dist/editor/animation-scene.d.ts.map +1 -1
  49. package/dist/editor/animation-scene.js +123 -0
  50. package/dist/editor/animation-scene.js.map +1 -1
  51. package/dist/editor/applies/create.d.ts +0 -6
  52. package/dist/editor/applies/create.d.ts.map +1 -1
  53. package/dist/editor/applies/create.js +1 -1
  54. package/dist/editor/applies/create.js.map +1 -1
  55. package/dist/editor/applies/link-move.d.ts +0 -8
  56. package/dist/editor/applies/link-move.d.ts.map +1 -1
  57. package/dist/editor/applies/link-move.js +1 -1
  58. package/dist/editor/applies/link-move.js.map +1 -1
  59. package/dist/editor/interaction-state.d.ts +184 -0
  60. package/dist/editor/interaction-state.d.ts.map +1 -0
  61. package/dist/editor/interaction-state.js +139 -0
  62. package/dist/editor/interaction-state.js.map +1 -0
  63. package/dist/editor/link-handle-drag.d.ts +152 -0
  64. package/dist/editor/link-handle-drag.d.ts.map +1 -0
  65. package/dist/editor/link-handle-drag.js +300 -0
  66. package/dist/editor/link-handle-drag.js.map +1 -0
  67. package/dist/editor/pointer-binding.d.ts +9 -11
  68. package/dist/editor/pointer-binding.d.ts.map +1 -1
  69. package/dist/editor/pointer-binding.js +1185 -901
  70. package/dist/editor/pointer-binding.js.map +1 -1
  71. package/dist/editor/public/brush.d.ts +31 -12
  72. package/dist/editor/public/brush.d.ts.map +1 -1
  73. package/dist/editor/public/brush.js +66 -17
  74. package/dist/editor/public/brush.js.map +1 -1
  75. package/dist/editor/public/cursor.d.ts +1 -15
  76. package/dist/editor/public/cursor.d.ts.map +1 -1
  77. package/dist/editor/public/cursor.js +14 -3
  78. package/dist/editor/public/cursor.js.map +1 -1
  79. package/dist/editor/public/eraser.d.ts +55 -0
  80. package/dist/editor/public/eraser.d.ts.map +1 -0
  81. package/dist/editor/public/eraser.js +65 -0
  82. package/dist/editor/public/eraser.js.map +1 -0
  83. package/dist/editor/public/laser.d.ts +38 -0
  84. package/dist/editor/public/laser.d.ts.map +1 -0
  85. package/dist/editor/public/laser.js +42 -0
  86. package/dist/editor/public/laser.js.map +1 -0
  87. package/dist/editor/public/placement.d.ts.map +1 -1
  88. package/dist/editor/public/placement.js +9 -1
  89. package/dist/editor/public/placement.js.map +1 -1
  90. package/dist/editor/public/selection-ops.d.ts +12 -0
  91. package/dist/editor/public/selection-ops.d.ts.map +1 -1
  92. package/dist/editor/public/selection-ops.js +19 -1
  93. package/dist/editor/public/selection-ops.js.map +1 -1
  94. package/dist/editor/public/stroke-eraser-coverage.d.ts +37 -0
  95. package/dist/editor/public/stroke-eraser-coverage.d.ts.map +1 -0
  96. package/dist/editor/public/stroke-eraser-coverage.js +168 -0
  97. package/dist/editor/public/stroke-eraser-coverage.js.map +1 -0
  98. package/dist/editor/public/stroke-eraser.d.ts +50 -0
  99. package/dist/editor/public/stroke-eraser.d.ts.map +1 -0
  100. package/dist/editor/public/stroke-eraser.js +176 -0
  101. package/dist/editor/public/stroke-eraser.js.map +1 -0
  102. package/dist/editor/public/stroke-smoothing.d.ts +21 -0
  103. package/dist/editor/public/stroke-smoothing.d.ts.map +1 -0
  104. package/dist/editor/public/stroke-smoothing.js +37 -0
  105. package/dist/editor/public/stroke-smoothing.js.map +1 -0
  106. package/dist/editor/public/tool-ops.d.ts +130 -0
  107. package/dist/editor/public/tool-ops.d.ts.map +1 -0
  108. package/dist/editor/public/tool-ops.js +392 -0
  109. package/dist/editor/public/tool-ops.js.map +1 -0
  110. package/dist/editor/public/z-order.d.ts +1 -12
  111. package/dist/editor/public/z-order.d.ts.map +1 -1
  112. package/dist/editor/public/z-order.js +1 -1
  113. package/dist/editor/public/z-order.js.map +1 -1
  114. package/dist/editor/public/zoom-pan.d.ts +9 -0
  115. package/dist/editor/public/zoom-pan.d.ts.map +1 -1
  116. package/dist/editor/public/zoom-pan.js +31 -0
  117. package/dist/editor/public/zoom-pan.js.map +1 -1
  118. package/dist/editor/render-orchestrator.d.ts +177 -5
  119. package/dist/editor/render-orchestrator.d.ts.map +1 -1
  120. package/dist/editor/render-orchestrator.js +517 -351
  121. package/dist/editor/render-orchestrator.js.map +1 -1
  122. package/dist/editor/text-edit.d.ts +136 -0
  123. package/dist/editor/text-edit.d.ts.map +1 -0
  124. package/dist/editor/text-edit.js +328 -0
  125. package/dist/editor/text-edit.js.map +1 -0
  126. package/dist/editor.d.ts +504 -239
  127. package/dist/editor.d.ts.map +1 -1
  128. package/dist/editor.js +1557 -706
  129. package/dist/editor.js.map +1 -1
  130. package/dist/handle.d.ts +0 -2
  131. package/dist/handle.d.ts.map +1 -1
  132. package/dist/handle.js +0 -6
  133. package/dist/handle.js.map +1 -1
  134. package/dist/index.d.ts +6 -1
  135. package/dist/index.d.ts.map +1 -1
  136. package/dist/index.js +7 -1
  137. package/dist/index.js.map +1 -1
  138. package/dist/modes.d.ts +15 -1
  139. package/dist/modes.d.ts.map +1 -1
  140. package/dist/modes.js.map +1 -1
  141. package/dist/overlay.d.ts +91 -2
  142. package/dist/overlay.d.ts.map +1 -1
  143. package/dist/overlay.js +450 -93
  144. package/dist/overlay.js.map +1 -1
  145. package/dist/search.d.ts +37 -0
  146. package/dist/search.d.ts.map +1 -0
  147. package/dist/search.js +45 -0
  148. package/dist/search.js.map +1 -0
  149. package/package.json +4 -4
@@ -0,0 +1,50 @@
1
+ import { type BrushElement, type Scene, type Patch } from "@oh-just-another/scene";
2
+ import { type ElementId, type Vec2 } from "@oh-just-another/types";
3
+ import { type Interval } from "./stroke-eraser-coverage.js";
4
+ /**
5
+ * Grow a brush's accumulated covered spans by the eraser segment `a → b` (world)
6
+ * at `radius` and return the merged result. INCREMENTAL: the live gesture calls
7
+ * this once per pointer move with only the new segment, so a drag costs
8
+ * O(points) per move instead of re-scanning the whole path each frame. `radius`
9
+ * is the visible cursor ring in world units. Pure — returns a new interval list.
10
+ */
11
+ export declare const markErasedIntervals: (brush: BrushElement, existing: readonly Interval[], a: Vec2, b: Vec2, radius: number) => Interval[];
12
+ /**
13
+ * STROKE-ERASER core (pure). Cut a single brush against an eraser path: the arc
14
+ * spans of the brush polyline within `radius` of the path are removed and the
15
+ * survivors split into fragment {@link BrushElement}s (endpoints on the ring).
16
+ * When nothing is covered, `erasedAny` is `false` and `fragments` is `[brush]`
17
+ * unchanged so the caller can skip the swap. One-shot (scans the whole path) —
18
+ * the live gesture uses the incremental {@link markErasedIntervals} instead.
19
+ */
20
+ export declare const computeEraseBrushStroke: (brush: BrushElement, eraserPathWorld: readonly Vec2[], radius: number, makeId: () => ElementId) => {
21
+ fragments: BrushElement[];
22
+ erasedAny: boolean;
23
+ };
24
+ /**
25
+ * Commit the accumulated stroke-erase: `erased` maps each touched brush id to the
26
+ * covered arc-length spans built incrementally by {@link markErasedIntervals}.
27
+ * Each such brush is replaced with its fragments (remove original + add fragments)
28
+ * and any link bound to it is detached (drop the binding — don't re-bind to a
29
+ * fragment). Returns the next scene + patches for ONE history batch, or `null`
30
+ * when nothing is cut.
31
+ */
32
+ export declare const computeEraseFromMasks: (scene: Scene, erased: ReadonlyMap<ElementId, readonly Interval[]>, makeId: () => ElementId) => {
33
+ readonly scene: Scene;
34
+ readonly patches: Patch[];
35
+ readonly removedIds: ElementId[];
36
+ readonly addedIds: ElementId[];
37
+ } | null;
38
+ /**
39
+ * Live-preview counterpart of {@link computeEraseFromMasks}: the cut fragments for
40
+ * every brush with covered spans so far, WITHOUT mutating the scene or consuming
41
+ * real ids. `fragments` are drawn on the overlay and `hidden` (the touched
42
+ * originals) suppressed in the main pass, so the user sees the cut before
43
+ * releasing. Fragment ids are deterministic per frame (never enter history).
44
+ * `null` when nothing is cut yet.
45
+ */
46
+ export declare const computeStrokeErasePreviewFromMasks: (scene: Scene, erased: ReadonlyMap<ElementId, readonly Interval[]>) => {
47
+ fragments: BrushElement[];
48
+ hidden: Set<ElementId>;
49
+ } | null;
50
+ //# sourceMappingURL=stroke-eraser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stroke-eraser.d.ts","sourceRoot":"","sources":["../../../src/editor/public/stroke-eraser.ts"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,YAAY,EAEjB,KAAK,KAAK,EACV,KAAK,KAAK,EACX,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAEL,KAAK,SAAS,EAEd,KAAK,IAAI,EACV,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAML,KAAK,QAAQ,EACd,MAAM,6BAA6B,CAAC;AA+DrC;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC9B,OAAO,YAAY,EACnB,UAAU,SAAS,QAAQ,EAAE,EAC7B,GAAG,IAAI,EACP,GAAG,IAAI,EACP,QAAQ,MAAM,KACb,QAAQ,EAIV,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAClC,OAAO,YAAY,EACnB,iBAAiB,SAAS,IAAI,EAAE,EAChC,QAAQ,MAAM,EACd,QAAQ,MAAM,SAAS,KACtB;IAAE,SAAS,EAAE,YAAY,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAajD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAChC,OAAO,KAAK,EACZ,QAAQ,WAAW,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC,EACnD,QAAQ,MAAM,SAAS,KACtB;IACD,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,SAAS,EAAE,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC;CAChC,GAAG,IAkCH,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,kCAAkC,GAC7C,OAAO,KAAK,EACZ,QAAQ,WAAW,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC,KAClD;IAAE,SAAS,EAAE,YAAY,EAAE,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;CAAE,GAAG,IAgB1D,CAAC"}
@@ -0,0 +1,176 @@
1
+ import { req } from "../../util.js";
2
+ import { addElement, removeElement, removeLink, orderBetween, isBrush, endpointElementId, } from "@oh-just-another/scene";
3
+ import { elementId as castElementId, } from "@oh-just-another/types";
4
+ import { ERASE_FRAGMENT_MIN_ARC } from "../../constants.js";
5
+ import { brushArc, coveredArcAgainstSegment, complementIntervals, mergeIntervals, localPointAtArc, } from "./stroke-eraser-coverage.js";
6
+ const EPS = 1e-6;
7
+ /**
8
+ * Rebuild a brush's surviving pieces from the COVERED arc-length spans the eraser
9
+ * removed. The kept spans (complement of `covered`) each become a fresh fragment
10
+ * {@link BrushElement}: the span endpoints — already pinned to the eraser ring by
11
+ * {@link coveredArcAgainstSegment}'s bisection — are the fragment's first/last
12
+ * points, with the original vertices strictly inside the span in between. Points
13
+ * are re-localised to the fragment's first world position, per-point widths kept,
14
+ * fractional `order`s around the original's slot, never `closed`.
15
+ *
16
+ * Cutting by ARC LENGTH (not vertex index) is what makes the eraser remove the
17
+ * geometry it visibly covers regardless of how densely the stroke was sampled — a
18
+ * big disc grazing a sparse line no longer slips between two far-apart vertices. A
19
+ * kept span shorter than {@link ERASE_FRAGMENT_MIN_ARC} is dropped (litter).
20
+ */
21
+ const buildFragmentsFromCoverage = (brush, covered, makeId) => {
22
+ const arc = brushArc(brush);
23
+ if (arc.total === 0)
24
+ return []; // a single-point brush leaves nothing once cut
25
+ const kept = complementIntervals(covered, arc.total);
26
+ const fragments = [];
27
+ let prevOrder = brush.order;
28
+ let idx = 0;
29
+ for (const [L0, L1] of kept) {
30
+ if (L1 - L0 < ERASE_FRAGMENT_MIN_ARC)
31
+ continue; // drop stray nubs
32
+ const runPts = [localPointAtArc(arc, L0)];
33
+ for (let i = 0; i < arc.pts.length; i++) {
34
+ const c = req(arc.cum[i]);
35
+ if (c > L0 + EPS && c < L1 - EPS)
36
+ runPts.push({ ...req(arc.pts[i]) });
37
+ }
38
+ runPts.push(localPointAtArc(arc, L1));
39
+ if (runPts.length < 2)
40
+ continue;
41
+ const anchor = req(runPts[0]);
42
+ const worldFirst = { x: brush.position.x + anchor.x, y: brush.position.y + anchor.y };
43
+ const localPoints = runPts.map((p) => ({
44
+ x: p.x - anchor.x,
45
+ y: p.y - anchor.y,
46
+ width: p.width,
47
+ }));
48
+ const order = idx === 0 ? brush.order : orderBetween(prevOrder, null);
49
+ prevOrder = order;
50
+ idx++;
51
+ fragments.push({
52
+ id: makeId(),
53
+ layerId: brush.layerId,
54
+ type: "brush",
55
+ position: worldFirst,
56
+ rotation: 0,
57
+ scale: { x: 1, y: 1 },
58
+ order,
59
+ style: brush.style,
60
+ points: localPoints,
61
+ });
62
+ }
63
+ return fragments;
64
+ };
65
+ /**
66
+ * Grow a brush's accumulated covered spans by the eraser segment `a → b` (world)
67
+ * at `radius` and return the merged result. INCREMENTAL: the live gesture calls
68
+ * this once per pointer move with only the new segment, so a drag costs
69
+ * O(points) per move instead of re-scanning the whole path each frame. `radius`
70
+ * is the visible cursor ring in world units. Pure — returns a new interval list.
71
+ */
72
+ export const markErasedIntervals = (brush, existing, a, b, radius) => {
73
+ const add = coveredArcAgainstSegment(brushArc(brush), a, b, radius);
74
+ if (add.length === 0)
75
+ return existing.length > 0 ? mergeIntervals(existing) : [];
76
+ return mergeIntervals([...existing, ...add]);
77
+ };
78
+ /**
79
+ * STROKE-ERASER core (pure). Cut a single brush against an eraser path: the arc
80
+ * spans of the brush polyline within `radius` of the path are removed and the
81
+ * survivors split into fragment {@link BrushElement}s (endpoints on the ring).
82
+ * When nothing is covered, `erasedAny` is `false` and `fragments` is `[brush]`
83
+ * unchanged so the caller can skip the swap. One-shot (scans the whole path) —
84
+ * the live gesture uses the incremental {@link markErasedIntervals} instead.
85
+ */
86
+ export const computeEraseBrushStroke = (brush, eraserPathWorld, radius, makeId) => {
87
+ if (eraserPathWorld.length === 0)
88
+ return { fragments: [brush], erasedAny: false };
89
+ const arc = brushArc(brush);
90
+ let covered = [];
91
+ const segs = Math.max(1, eraserPathWorld.length - 1);
92
+ for (let i = 0; i < segs; i++) {
93
+ const a = req(eraserPathWorld[i]);
94
+ const b = req(eraserPathWorld[Math.min(i + 1, eraserPathWorld.length - 1)]);
95
+ const add = coveredArcAgainstSegment(arc, a, b, radius);
96
+ if (add.length > 0)
97
+ covered = mergeIntervals([...covered, ...add]);
98
+ }
99
+ if (covered.length === 0)
100
+ return { fragments: [brush], erasedAny: false };
101
+ return { fragments: buildFragmentsFromCoverage(brush, covered, makeId), erasedAny: true };
102
+ };
103
+ /**
104
+ * Commit the accumulated stroke-erase: `erased` maps each touched brush id to the
105
+ * covered arc-length spans built incrementally by {@link markErasedIntervals}.
106
+ * Each such brush is replaced with its fragments (remove original + add fragments)
107
+ * and any link bound to it is detached (drop the binding — don't re-bind to a
108
+ * fragment). Returns the next scene + patches for ONE history batch, or `null`
109
+ * when nothing is cut.
110
+ */
111
+ export const computeEraseFromMasks = (scene, erased, makeId) => {
112
+ let s = scene;
113
+ const patches = [];
114
+ const removedIds = [];
115
+ const addedIds = [];
116
+ const droppedLinks = new Set();
117
+ for (const [brushId, covered] of erased) {
118
+ if (covered.length === 0)
119
+ continue;
120
+ const el = scene.elements.get(brushId);
121
+ if (el === undefined || !isBrush(el))
122
+ continue;
123
+ const fragments = buildFragmentsFromCoverage(el, covered, makeId);
124
+ // Detach links bound to this brush before removing it.
125
+ for (const edge of [...s.links.values()]) {
126
+ if (droppedLinks.has(edge.id))
127
+ continue;
128
+ if (endpointElementId(edge.from) === el.id || endpointElementId(edge.to) === el.id) {
129
+ const r = removeLink(s, edge.id);
130
+ s = r.scene;
131
+ patches.push(r.patch);
132
+ droppedLinks.add(edge.id);
133
+ }
134
+ }
135
+ const removed = removeElement(s, el.id);
136
+ s = removed.scene;
137
+ patches.push(removed.patch);
138
+ removedIds.push(el.id);
139
+ for (const frag of fragments) {
140
+ const added = addElement(s, frag);
141
+ s = added.scene;
142
+ patches.push(added.patch);
143
+ addedIds.push(frag.id);
144
+ }
145
+ }
146
+ if (patches.length === 0)
147
+ return null;
148
+ return { scene: s, patches, removedIds, addedIds };
149
+ };
150
+ /**
151
+ * Live-preview counterpart of {@link computeEraseFromMasks}: the cut fragments for
152
+ * every brush with covered spans so far, WITHOUT mutating the scene or consuming
153
+ * real ids. `fragments` are drawn on the overlay and `hidden` (the touched
154
+ * originals) suppressed in the main pass, so the user sees the cut before
155
+ * releasing. Fragment ids are deterministic per frame (never enter history).
156
+ * `null` when nothing is cut yet.
157
+ */
158
+ export const computeStrokeErasePreviewFromMasks = (scene, erased) => {
159
+ const fragments = [];
160
+ const hidden = new Set();
161
+ for (const [brushId, covered] of erased) {
162
+ if (covered.length === 0)
163
+ continue;
164
+ const el = scene.elements.get(brushId);
165
+ if (el === undefined || !isBrush(el))
166
+ continue;
167
+ let n = 0;
168
+ const parts = buildFragmentsFromCoverage(el, covered, () => castElementId(`stroke-erase-preview-${el.id}-${n++}`));
169
+ hidden.add(el.id);
170
+ fragments.push(...parts);
171
+ }
172
+ if (hidden.size === 0)
173
+ return null;
174
+ return { fragments, hidden };
175
+ };
176
+ //# sourceMappingURL=stroke-eraser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stroke-eraser.js","sourceRoot":"","sources":["../../../src/editor/public/stroke-eraser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { type Vec2 } from "@oh-just-another/types";
2
+ /** Any point that carries at least a world position. */
3
+ export interface SmoothablePoint {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ }
7
+ /**
8
+ * Resample a sparse polyline into a smooth, dense point list by fitting a
9
+ * Catmull-Rom spline through the captured points (reusing `catmullRomBeziers`
10
+ * from `@oh-just-another/scene` — one spline implementation for the whole repo)
11
+ * and sampling `perSegment` sub-points per span.
12
+ *
13
+ * Freehand input (laser trail, brush stroke) is captured sparsely — one point
14
+ * per pointer-move — so the raw polyline is angular. The `blend` callback
15
+ * rebuilds each resampled point from its span endpoints and the interpolation
16
+ * parameter `u`, so per-point payload beyond position (timestamp for the laser,
17
+ * width for the brush) is carried across smoothly. Fewer than three points
18
+ * can't form a curve, so they pass through unchanged.
19
+ */
20
+ export declare const smoothStrokePoints: <P extends SmoothablePoint>(points: readonly P[], perSegment: number, blend: (a: P, b: P, pos: Vec2, u: number) => P) => P[];
21
+ //# sourceMappingURL=stroke-smoothing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stroke-smoothing.d.ts","sourceRoot":"","sources":["../../../src/editor/public/stroke-smoothing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAIxD,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,eAAe,EAC1D,QAAQ,SAAS,CAAC,EAAE,EACpB,YAAY,MAAM,EAClB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC,KAC7C,CAAC,EAkBH,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { req } from "@oh-just-another/types";
2
+ import { bezier } from "@oh-just-another/math";
3
+ import { catmullRomBeziers } from "@oh-just-another/scene";
4
+ /**
5
+ * Resample a sparse polyline into a smooth, dense point list by fitting a
6
+ * Catmull-Rom spline through the captured points (reusing `catmullRomBeziers`
7
+ * from `@oh-just-another/scene` — one spline implementation for the whole repo)
8
+ * and sampling `perSegment` sub-points per span.
9
+ *
10
+ * Freehand input (laser trail, brush stroke) is captured sparsely — one point
11
+ * per pointer-move — so the raw polyline is angular. The `blend` callback
12
+ * rebuilds each resampled point from its span endpoints and the interpolation
13
+ * parameter `u`, so per-point payload beyond position (timestamp for the laser,
14
+ * width for the brush) is carried across smoothly. Fewer than three points
15
+ * can't form a curve, so they pass through unchanged.
16
+ */
17
+ export const smoothStrokePoints = (points, perSegment, blend) => {
18
+ if (points.length < 3)
19
+ return points;
20
+ const segments = catmullRomBeziers(points.map((p) => ({ x: p.x, y: p.y })));
21
+ const first = req(points[0]);
22
+ const out = [first];
23
+ let prev = { x: first.x, y: first.y };
24
+ for (let i = 0; i < segments.length; i++) {
25
+ const seg = req(segments[i]);
26
+ const a = req(points[i]);
27
+ const b = req(points[i + 1]);
28
+ for (let j = 1; j <= perSegment; j++) {
29
+ const u = j / perSegment;
30
+ const p = bezier.cubicAt(prev, seg.c1, seg.c2, seg.to, u);
31
+ out.push(blend(a, b, p, u));
32
+ }
33
+ prev = seg.to;
34
+ }
35
+ return out;
36
+ };
37
+ //# sourceMappingURL=stroke-smoothing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stroke-smoothing.js","sourceRoot":"","sources":["../../../src/editor/public/stroke-smoothing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAQ3D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,MAAoB,EACpB,UAAkB,EAClB,KAA8C,EACzC,EAAE;IACP,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,MAAa,CAAC;IAC5C,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAQ,CAAC,KAAK,CAAC,CAAC;IACzB,IAAI,IAAI,GAAS,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,MAAM,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC;YACzB,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;YAC1D,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC;QACD,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;IAChB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC"}
@@ -0,0 +1,130 @@
1
+ import { type Element, type ImageCrop, type Link, type Patch, type Scene } from "@oh-just-another/scene";
2
+ import type { Bounds, Color, ElementId, LinkId, Vec2 } from "@oh-just-another/types";
3
+ /**
4
+ * The style colour of the top-most shape under `point`, or `null` when the
5
+ * point is empty. `role` chooses which channel to sample: `"fill"` (default)
6
+ * falls back to the stroke when there is no fill, and `"stroke"` the other way
7
+ * round — so clicking a stroke-only outline still yields a usable colour.
8
+ */
9
+ export declare const pickColorAt: (scene: Scene, point: Vec2, role?: "fill" | "stroke") => Color | null;
10
+ /** Target types accepted by {@link computeConvertType}. `"polygon"` = diamond. */
11
+ export type ConvertTarget = "rectangle" | "ellipse" | "polygon";
12
+ /**
13
+ * Convert every convertible shape in `ids` to `target`, preserving
14
+ * position / rotation / scale / style and the on-canvas footprint. rectangle
15
+ * and ellipse carry `width` × `height`; `"polygon"` renders a diamond
16
+ * inscribed in that box. Shapes already of the target type, and non-convertible
17
+ * types (text, image, …), are skipped. One undo step (batched for 2+). Returns
18
+ * `null` when nothing changed.
19
+ */
20
+ export declare const computeConvertType: (scene: Scene, ids: Iterable<ElementId>, target: ConvertTarget) => {
21
+ readonly scene: Scene;
22
+ readonly patch: Patch;
23
+ } | null;
24
+ /** Clamp a crop rect into the valid `[0,1]` normalised range (non-empty). */
25
+ export declare const clampCrop: (crop: ImageCrop) => ImageCrop;
26
+ /**
27
+ * Set (or clear, with `null`) the normalised crop of the image `id`. No-op
28
+ * (returns `null`) when the shape is missing or not an image, or when the
29
+ * crop is unchanged. Setting a full-image crop (`0,0,1,1`) clears the field.
30
+ */
31
+ export declare const computeSetImageCrop: (scene: Scene, id: ElementId, crop: ImageCrop | null) => {
32
+ readonly scene: Scene;
33
+ readonly patch: Patch;
34
+ } | null;
35
+ /**
36
+ * Normalised crop rect covering the whole image. Equivalent to no crop.
37
+ */
38
+ export declare const FULL_CROP: ImageCrop;
39
+ /** The 8 crop-frame handles: 4 corners + 4 edge midpoints. */
40
+ export type CropHandle = "nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w";
41
+ /** Ordered so corner handles win a tie against the adjacent edge midpoints. */
42
+ export declare const CROP_HANDLES: readonly CropHandle[];
43
+ /**
44
+ * Local-space rect the WHOLE (uncropped) bitmap would occupy, aligned so its
45
+ * current crop-window equals the element's box `{0,0,width,height}`. The
46
+ * element currently shows the sub-rectangle `crop` of this virtual full image.
47
+ * Used to draw the crop ghost and to clamp handle drags to the image extent.
48
+ */
49
+ export declare const cropFullImageLocalRect: (el: Element, crop: ImageCrop) => Bounds;
50
+ /**
51
+ * World-space positions of the 8 crop handles for the element's current box
52
+ * `{0,0,width,height}` (the visible crop window). Honours rotation / scale via
53
+ * `localToWorld`. The window always equals the element bounds, so the crop
54
+ * fraction is not needed here — pass the (pending) element whose `width` /
55
+ * `height` / `position` describe the window.
56
+ */
57
+ export declare const cropHandleWorldPoints: (el: Element) => Record<CropHandle, Vec2>;
58
+ /**
59
+ * Result of a crop handle / body drag: the pending normalised `crop` plus the
60
+ * element's new world `position` and local `width` × `height`. Rotation / scale
61
+ * are unchanged, so the caller keeps them.
62
+ */
63
+ export interface CropDragResult {
64
+ readonly crop: ImageCrop;
65
+ readonly position: Vec2;
66
+ readonly width: number;
67
+ readonly height: number;
68
+ }
69
+ /**
70
+ * Drag one crop handle to `worldPoint`. Moves the window edge(s) that handle
71
+ * controls (the opposite edge stays fixed), clamped so the window stays inside
72
+ * the virtual full image and never shrinks below {@link CROP_MIN_SIZE}. The
73
+ * source is never stretched: shrinking the window hides pixels rather than
74
+ * scaling them. `el` must be the ORIGINAL (unmutated) element so the reference
75
+ * frame stays stable across a multi-move drag.
76
+ */
77
+ export declare const computeCropHandleDrag: (el: Element, crop: ImageCrop, handle: CropHandle, worldPoint: Vec2) => CropDragResult;
78
+ /**
79
+ * Pan the source under a FIXED window: the element's box (position / size) is
80
+ * unchanged; only which region of the bitmap shows moves. `worldDelta` from
81
+ * `dragStartWorld` → `worldPoint` is projected into local space and converted
82
+ * to a crop shift, clamped so the window stays inside the image. `el` must be
83
+ * the ORIGINAL element and `crop` its ORIGINAL crop (drag-start snapshot).
84
+ */
85
+ export declare const computeCropBodyPan: (el: Element, crop: ImageCrop, dragStartWorld: Vec2, worldPoint: Vec2) => {
86
+ readonly crop: ImageCrop;
87
+ };
88
+ /**
89
+ * Commit a pending crop drag: write the normalised `crop` AND the new element
90
+ * `position` / `width` / `height` in a single patch (one undo step). A crop
91
+ * that covers the whole image is stored as no crop (field cleared). No-op
92
+ * (`null`) for a missing / non-image shape or when nothing changed.
93
+ */
94
+ export declare const computeCommitImageCrop: (scene: Scene, id: ElementId, next: CropDragResult) => {
95
+ readonly scene: Scene;
96
+ readonly patch: Patch;
97
+ } | null;
98
+ export type SpawnDirection = "left" | "right" | "up" | "down";
99
+ /**
100
+ * Create a clone of `sourceId` offset by {@link SPAWN_CONNECTED_GAP_PX} in
101
+ * `direction`, plus a link from the source to the new node. The clone keeps the
102
+ * source's type, size, style and rotation. Returns the next scene, both patches
103
+ * (element add + link add), and the new ids; `null` when the source is missing.
104
+ */
105
+ export declare const computeSpawnConnectedNode: (scene: Scene, sourceId: ElementId, direction: SpawnDirection, newElementId: ElementId, newLinkId: LinkId) => {
106
+ readonly scene: Scene;
107
+ readonly patches: readonly Patch[];
108
+ readonly newElementId: ElementId;
109
+ readonly linkId: LinkId;
110
+ } | null;
111
+ /**
112
+ * Pure geometry for a flowchart CREATE session: `count` clones of `sourceId`,
113
+ * each linked source→clone (floating). Does NOT mutate `scene` or history —
114
+ * returns the pending `elements` + `links` for a PREVIEW; the caller commits
115
+ * them later. Placement matches Excalidraw's `addNewNodes`:
116
+ *
117
+ * - `left` / `right` — the `count` siblings stack vertically, centred on the
118
+ * source (step = source height + {@link SPAWN_CONNECTED_GAP_PX}), all offset
119
+ * by one source-width + gap along the axis.
120
+ * - `up` / `down` — siblings spread horizontally, centred on the source
121
+ * (step = source width + gap), all offset by one source-height + gap.
122
+ *
123
+ * `count === 1` reproduces {@link computeSpawnConnectedNode}'s single placement
124
+ * (zero perpendicular offset). Returns empty arrays when the source is missing.
125
+ */
126
+ export declare const computeSpawnConnectedNodes: (scene: Scene, sourceId: ElementId, direction: SpawnDirection, count: number, makeElementId: () => ElementId, makeLinkId: () => LinkId) => {
127
+ readonly elements: Element[];
128
+ readonly links: Link[];
129
+ };
130
+ //# sourceMappingURL=tool-ops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-ops.d.ts","sourceRoot":"","sources":["../../../src/editor/public/tool-ops.ts"],"names":[],"mappings":"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"}