@rasamjs/tools 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/API.md +308 -0
  2. package/CHANGELOG.md +53 -0
  3. package/LICENSE +21 -0
  4. package/README.md +138 -2
  5. package/RELEASING.md +88 -0
  6. package/dist/advanced/context.d.ts +15 -0
  7. package/dist/advanced/context.d.ts.map +1 -0
  8. package/dist/advanced/context.js +2 -0
  9. package/dist/advanced/context.js.map +1 -0
  10. package/dist/advanced/curves.d.ts +5 -0
  11. package/dist/advanced/curves.d.ts.map +1 -0
  12. package/dist/advanced/curves.js +65 -0
  13. package/dist/advanced/curves.js.map +1 -0
  14. package/dist/advanced/geometry.d.ts +17 -0
  15. package/dist/advanced/geometry.d.ts.map +1 -0
  16. package/dist/advanced/geometry.js +61 -0
  17. package/dist/advanced/geometry.js.map +1 -0
  18. package/dist/advanced/rotation.d.ts +4 -0
  19. package/dist/advanced/rotation.d.ts.map +1 -0
  20. package/dist/advanced/rotation.js +86 -0
  21. package/dist/advanced/rotation.js.map +1 -0
  22. package/dist/advanced/session.d.ts +5 -0
  23. package/dist/advanced/session.d.ts.map +1 -0
  24. package/dist/advanced/session.js +240 -0
  25. package/dist/advanced/session.js.map +1 -0
  26. package/dist/advanced/snapper.d.ts +29 -0
  27. package/dist/advanced/snapper.d.ts.map +1 -0
  28. package/dist/advanced/snapper.js +118 -0
  29. package/dist/advanced/snapper.js.map +1 -0
  30. package/dist/advanced/types.d.ts +30 -0
  31. package/dist/advanced/types.d.ts.map +1 -0
  32. package/dist/advanced/types.js +2 -0
  33. package/dist/advanced/types.js.map +1 -0
  34. package/dist/drawing.d.ts +5 -0
  35. package/dist/drawing.d.ts.map +1 -0
  36. package/dist/drawing.js +3 -0
  37. package/dist/drawing.js.map +1 -0
  38. package/dist/editor/draw-tool.d.ts +35 -0
  39. package/dist/editor/draw-tool.d.ts.map +1 -0
  40. package/dist/editor/draw-tool.js +174 -0
  41. package/dist/editor/draw-tool.js.map +1 -0
  42. package/dist/editor/path-editor.d.ts +34 -0
  43. package/dist/editor/path-editor.d.ts.map +1 -0
  44. package/dist/editor/path-editor.js +208 -0
  45. package/dist/editor/path-editor.js.map +1 -0
  46. package/dist/editor/pointer.d.ts +21 -0
  47. package/dist/editor/pointer.d.ts.map +1 -0
  48. package/dist/editor/pointer.js +86 -0
  49. package/dist/editor/pointer.js.map +1 -0
  50. package/dist/editor/surface.d.ts +31 -0
  51. package/dist/editor/surface.d.ts.map +1 -0
  52. package/dist/editor/surface.js +138 -0
  53. package/dist/editor/surface.js.map +1 -0
  54. package/dist/grid.d.ts +23 -0
  55. package/dist/grid.d.ts.map +1 -0
  56. package/dist/grid.js +84 -0
  57. package/dist/grid.js.map +1 -0
  58. package/dist/rulers.d.ts +30 -0
  59. package/dist/rulers.d.ts.map +1 -0
  60. package/dist/rulers.js +234 -0
  61. package/dist/rulers.js.map +1 -0
  62. package/dist/snapping/advanced.d.ts +3 -0
  63. package/dist/snapping/advanced.d.ts.map +1 -0
  64. package/dist/snapping/advanced.js +2 -0
  65. package/dist/snapping/advanced.js.map +1 -0
  66. package/dist/snapping/guides.d.ts +14 -0
  67. package/dist/snapping/guides.d.ts.map +1 -0
  68. package/dist/snapping/guides.js +109 -0
  69. package/dist/snapping/guides.js.map +1 -0
  70. package/dist/snapping/lines.d.ts +43 -0
  71. package/dist/snapping/lines.d.ts.map +1 -0
  72. package/dist/snapping/lines.js +74 -0
  73. package/dist/snapping/lines.js.map +1 -0
  74. package/dist/snapping/options.d.ts +18 -0
  75. package/dist/snapping/options.d.ts.map +1 -0
  76. package/dist/snapping/options.js +44 -0
  77. package/dist/snapping/options.js.map +1 -0
  78. package/dist/snapping/overlay.d.ts +22 -0
  79. package/dist/snapping/overlay.d.ts.map +1 -0
  80. package/dist/snapping/overlay.js +165 -0
  81. package/dist/snapping/overlay.js.map +1 -0
  82. package/dist/snapping/session.d.ts +25 -0
  83. package/dist/snapping/session.d.ts.map +1 -0
  84. package/dist/snapping/session.js +89 -0
  85. package/dist/snapping/session.js.map +1 -0
  86. package/dist/snapping/snapper.d.ts +22 -0
  87. package/dist/snapping/snapper.d.ts.map +1 -0
  88. package/dist/snapping/snapper.js +140 -0
  89. package/dist/snapping/snapper.js.map +1 -0
  90. package/dist/snapping/spacing.d.ts +27 -0
  91. package/dist/snapping/spacing.d.ts.map +1 -0
  92. package/dist/snapping/spacing.js +127 -0
  93. package/dist/snapping/spacing.js.map +1 -0
  94. package/dist/snapping/targets.d.ts +7 -0
  95. package/dist/snapping/targets.d.ts.map +1 -0
  96. package/dist/snapping/targets.js +71 -0
  97. package/dist/snapping/targets.js.map +1 -0
  98. package/dist/snapping/types.d.ts +35 -0
  99. package/dist/snapping/types.d.ts.map +1 -0
  100. package/dist/snapping/types.js +2 -0
  101. package/dist/snapping/types.js.map +1 -0
  102. package/dist/snapping.d.ts +4 -0
  103. package/dist/snapping.d.ts.map +1 -0
  104. package/dist/snapping.js +3 -0
  105. package/dist/snapping.js.map +1 -0
  106. package/package.json +73 -4
  107. package/src/advanced/context.ts +15 -0
  108. package/src/advanced/curves.ts +65 -0
  109. package/src/advanced/geometry.ts +74 -0
  110. package/src/advanced/rotation.ts +93 -0
  111. package/src/advanced/session.ts +276 -0
  112. package/src/advanced/snapper.ts +124 -0
  113. package/src/advanced/types.ts +31 -0
  114. package/src/drawing.ts +4 -0
  115. package/src/editor/draw-tool.ts +190 -0
  116. package/src/editor/path-editor.ts +224 -0
  117. package/src/editor/pointer.ts +85 -0
  118. package/src/editor/surface.ts +141 -0
  119. package/src/grid.ts +99 -0
  120. package/src/rulers.ts +245 -0
  121. package/src/snapping/advanced.ts +8 -0
  122. package/src/snapping/guides.ts +132 -0
  123. package/src/snapping/lines.ts +108 -0
  124. package/src/snapping/options.ts +47 -0
  125. package/src/snapping/overlay.ts +186 -0
  126. package/src/snapping/session.ts +116 -0
  127. package/src/snapping/snapper.ts +147 -0
  128. package/src/snapping/spacing.ts +157 -0
  129. package/src/snapping/targets.ts +88 -0
  130. package/src/snapping/types.ts +27 -0
  131. package/src/snapping.ts +3 -0
@@ -0,0 +1,127 @@
1
+ import { lowerBound } from './lines.js';
2
+ const extent = { x: 'width', y: 'height' };
3
+ const end = (box, axis) => box[axis] + box[extent[axis]];
4
+ const across = (axis) => (axis === 'x' ? 'y' : 'x');
5
+ const overlaps = (a, b, axis) => a[axis] <= end(b, axis) && b[axis] <= end(a, axis);
6
+ const middle = (a, b, axis) => (Math.max(a[axis], b[axis]) + Math.min(end(a, axis), end(b, axis))) / 2;
7
+ /**
8
+ * Only gaps after the furthest preceding overlapping box are unobstructed. Boxes that
9
+ * contain the next box on this axis are backgrounds, not obstructions.
10
+ */
11
+ function referenceGaps(targets, axis) {
12
+ const gaps = [];
13
+ // Far edges ascending; equal edges scan the earliest document target first.
14
+ const ordered = targets
15
+ .map((box, order) => ({ box, order, value: end(box, axis) }))
16
+ .sort((a, b) => a.value - b.value || b.order - a.order);
17
+ for (const next of targets)
18
+ for (let index = lowerBound(ordered, end(next, axis)) - 1; index >= 0; index--) {
19
+ const previous = ordered[index];
20
+ if (previous.box[axis] >= next[axis] || !overlaps(previous.box, next, across(axis)))
21
+ continue;
22
+ if (previous.value < next[axis])
23
+ gaps.push({
24
+ from: previous.value,
25
+ to: next[axis],
26
+ at: middle(previous.box, next, across(axis)),
27
+ });
28
+ break;
29
+ }
30
+ return gaps;
31
+ }
32
+ function projectedBox(setup, offset, centered = setup.centered) {
33
+ const box = { ...setup.box };
34
+ for (const axis of ['x', 'y']) {
35
+ if (setup.kind !== 'resize') {
36
+ box[axis] += offset[axis];
37
+ continue;
38
+ }
39
+ const feature = setup.features[axis][0];
40
+ if (!feature)
41
+ continue;
42
+ const start = setup.box[axis], finish = end(setup.box, axis);
43
+ const fixed = Math.abs(feature.value - start) < Math.abs(feature.value - finish) ? finish : start;
44
+ const opposite = fixed - (centered ? offset[axis] : 0), active = feature.value + offset[axis];
45
+ box[axis] = Math.min(active, opposite);
46
+ box[extent[axis]] = Math.abs(active - opposite);
47
+ }
48
+ return box;
49
+ }
50
+ /** Targets that contain the box on this axis (backgrounds, panels) are not neighbors. */
51
+ function neighbors(box, targets, axis) {
52
+ let left = -1, right = -1;
53
+ targets.forEach((target, index) => {
54
+ if (!overlaps(box, target, across(axis)))
55
+ return;
56
+ const before = target[axis] <= box[axis], after = end(target, axis) >= end(box, axis);
57
+ if (before && after)
58
+ return;
59
+ if (before && (left < 0 || end(target, axis) > end(targets[left], axis)))
60
+ left = index;
61
+ if (after && (right < 0 || target[axis] < targets[right][axis]))
62
+ right = index;
63
+ });
64
+ return [left, right];
65
+ }
66
+ /** Snapshot once; pointer queries scan cached boxes/gaps without any DOM reads. */
67
+ export function createSpacing(input) {
68
+ const setup = {
69
+ ...input,
70
+ box: { ...input.box },
71
+ targets: input.targets.map((box) => ({ ...box })),
72
+ };
73
+ const references = { x: referenceGaps(setup.targets, 'x'), y: referenceGaps(setup.targets, 'y') };
74
+ if (!references.x.length && !references.y.length)
75
+ return null;
76
+ return ({ axis, offset, tolerance, centered = setup.centered }) => {
77
+ const resizing = setup.kind === 'resize', feature = setup.features[axis][0];
78
+ if (resizing && !feature)
79
+ return null;
80
+ const box = projectedBox(setup, offset, centered), [left, right] = neighbors(box, setup.targets, axis);
81
+ const active = feature && feature.value + offset[axis];
82
+ const dragged = resizing && active > box[axis] ? 'after' : 'before';
83
+ let best = null;
84
+ const mark = (next, side) => {
85
+ const index = side === 'before' ? left : right, neighbor = setup.targets[index];
86
+ return {
87
+ from: side === 'before' ? end(neighbor, axis) : end(next, axis),
88
+ to: side === 'before' ? next[axis] : neighbor[axis],
89
+ at: middle(next, neighbor, across(axis)),
90
+ moving: side === 'before' ? 'to' : 'from',
91
+ neighbor: index,
92
+ };
93
+ };
94
+ const offer = (delta, side, reference) => {
95
+ if (Math.abs(delta) > tolerance || (best && Math.abs(delta) >= Math.abs(best.delta)))
96
+ return;
97
+ const next = projectedBox(setup, { ...offset, [axis]: offset[axis] + delta }, centered);
98
+ const target = side === 'before' ? next[axis] : end(next, axis);
99
+ if (resizing && Math.abs(active + delta - target) > 1e-7)
100
+ return;
101
+ const gaps = reference
102
+ ? [reference, mark(next, side)]
103
+ : [mark(next, 'before'), mark(next, 'after')];
104
+ const length = gaps[0].to - gaps[0].from;
105
+ if (length <= 0 ||
106
+ gaps.some((gap) => Math.abs(gap.to - gap.from - length) > 1e-7 * Math.max(1, length)))
107
+ return;
108
+ best = { delta, target, order: side === 'before' ? left : right, grid: false, gaps };
109
+ };
110
+ if (left >= 0 && right >= 0 && left !== right) {
111
+ const before = box[axis] - end(setup.targets[left], axis), after = setup.targets[right][axis] - end(box, axis);
112
+ let delta = (after - before) / 2;
113
+ if (resizing)
114
+ delta = centered ? 0 : after - before;
115
+ offer(delta, dragged);
116
+ }
117
+ for (const reference of references[axis]) {
118
+ const length = reference.to - reference.from;
119
+ if (left >= 0 && (!resizing || dragged === 'before'))
120
+ offer(end(setup.targets[left], axis) + length - box[axis], 'before', reference);
121
+ if (right >= 0 && (!resizing || dragged === 'after'))
122
+ offer(setup.targets[right][axis] - length - end(box, axis), 'after', reference);
123
+ }
124
+ return best;
125
+ };
126
+ }
127
+ //# sourceMappingURL=spacing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spacing.js","sourceRoot":"","sources":["../../src/snapping/spacing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { AnyNode, Box, Svg } from '@rasamjs/core';
2
+ import type { ResolvedOptions } from './options.js';
3
+ export declare function validateNodes(nodes: readonly AnyNode[], drawing: Svg): void;
4
+ export declare function movingBox(nodes: readonly AnyNode[], drawing: Svg): Box | null;
5
+ export declare function targetNodes(drawing: Svg, nodes: readonly AnyNode[], options: ResolvedOptions): AnyNode[];
6
+ export declare function measureTargets(drawing: Svg, nodes: readonly AnyNode[], options: ResolvedOptions): Box[];
7
+ //# sourceMappingURL=targets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../src/snapping/targets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,GAAG,IAAI,CAW3E;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI,CAgB7E;AAWD,wBAAgB,WAAW,CACzB,OAAO,EAAE,GAAG,EACZ,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,OAAO,EAAE,eAAe,GACvB,OAAO,EAAE,CAwBX;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,GAAG,EACZ,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,OAAO,EAAE,eAAe,GACvB,GAAG,EAAE,CAQP"}
@@ -0,0 +1,71 @@
1
+ import { Guards } from '@rasamjs/core/advanced';
2
+ import { boundsIn } from '@rasamjs/core/viewport';
3
+ export function validateNodes(nodes, drawing) {
4
+ if (!Array.isArray(nodes))
5
+ throw new TypeError('Snap nodes must be an array');
6
+ for (const node of nodes)
7
+ if (!Guards.isTransformable(node) ||
8
+ node.isDestroyed ||
9
+ node === drawing ||
10
+ node.context !== drawing.context ||
11
+ !drawing.el.contains(node.el))
12
+ throw new TypeError('Snap nodes must be live shapes inside this drawing');
13
+ }
14
+ export function movingBox(nodes, drawing) {
15
+ let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
16
+ for (const node of nodes) {
17
+ const bounds = boundsIn(node, drawing);
18
+ if (bounds.status === 'unavailable')
19
+ return null;
20
+ if (!('box' in bounds))
21
+ continue;
22
+ const box = bounds.box;
23
+ left = Math.min(left, box.x);
24
+ top = Math.min(top, box.y);
25
+ right = Math.max(right, box.x + box.width);
26
+ bottom = Math.max(bottom, box.y + box.height);
27
+ }
28
+ return left === Infinity ? null : { x: left, y: top, width: right - left, height: bottom - top };
29
+ }
30
+ function visible(node) {
31
+ const view = node.el.ownerDocument.defaultView;
32
+ const visibility = view.getComputedStyle(node.el).visibility;
33
+ if (visibility === 'hidden' || visibility === 'collapse')
34
+ return false;
35
+ for (let element = node.el; element; element = element.parentElement)
36
+ if (view.getComputedStyle(element).display === 'none')
37
+ return false;
38
+ return true;
39
+ }
40
+ export function targetNodes(drawing, nodes, options) {
41
+ const candidates = options.targets
42
+ ? options.targets(nodes)
43
+ : nodes.length
44
+ ? nodes.flatMap((node) => node.parent?.children ?? [])
45
+ : drawing.children;
46
+ const eligible = new Set();
47
+ for (const node of candidates) {
48
+ if (!Guards.isTransformable(node) ||
49
+ node.isDestroyed ||
50
+ node.context !== drawing.context ||
51
+ !drawing.el.contains(node.el) ||
52
+ nodes.some((selected) => node.el.contains(selected.el) || selected.el.contains(node.el)) ||
53
+ !visible(node))
54
+ continue;
55
+ eligible.add(node);
56
+ }
57
+ // All eligible nodes are attached to the same document; bit 4 means FOLLOWING.
58
+ const ordered = [...eligible].sort((a, b) => a === b ? 0 : a.el.compareDocumentPosition(b.el) & 4 ? -1 : 1);
59
+ return ordered;
60
+ }
61
+ export function measureTargets(drawing, nodes, options) {
62
+ const boxes = [];
63
+ const ordered = targetNodes(drawing, nodes, options);
64
+ for (const node of ordered) {
65
+ const bounds = boundsIn(node, drawing);
66
+ if ('box' in bounds)
67
+ boxes.push(bounds.box);
68
+ }
69
+ return boxes;
70
+ }
71
+ //# sourceMappingURL=targets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targets.js","sourceRoot":"","sources":["../../src/snapping/targets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { AnyNode, Point } from '@rasamjs/core';
2
+ export interface SnapperOptions {
3
+ readonly tolerance?: number;
4
+ readonly shapes?: boolean;
5
+ readonly spacing?: boolean;
6
+ readonly grid?: number | {
7
+ readonly size: number;
8
+ readonly origin?: Point;
9
+ } | null;
10
+ readonly targets?: (nodes: readonly AnyNode[]) => Iterable<AnyNode>;
11
+ readonly modifier?: 'ctrl' | 'none';
12
+ readonly style?: {
13
+ readonly stroke?: string;
14
+ readonly gapStroke?: string;
15
+ };
16
+ }
17
+ export type SnapRequest = {
18
+ readonly kind: 'point';
19
+ readonly client: Point;
20
+ } | {
21
+ readonly kind: 'move' | 'resize';
22
+ readonly nodes: readonly AnyNode[];
23
+ readonly client: Point;
24
+ readonly corners: readonly Point[];
25
+ readonly anchor?: Point;
26
+ readonly centered?: boolean;
27
+ };
28
+ export interface SnapSession {
29
+ snap(client: Point, modifiers: {
30
+ readonly ignore: boolean;
31
+ readonly centered?: boolean;
32
+ }): Point;
33
+ end(): void;
34
+ }
35
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/snapping/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAA;KAAE,GAAG,IAAI,CAAC;IACnF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,KAAK,QAAQ,CAAC,OAAO,CAAC,CAAC;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5E;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAA;CAAE,GAClD;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEN,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,KAAK,CAAC;IACjG,GAAG,IAAI,IAAI,CAAC;CACb"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/snapping/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ /** Opt-in snapping tools for rasam drawings. */
2
+ export { Snapper } from './snapping/snapper.js';
3
+ export type { SnapperOptions, SnapRequest, SnapSession } from './snapping/types.js';
4
+ //# sourceMappingURL=snapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapping.d.ts","sourceRoot":"","sources":["../src/snapping.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Opt-in snapping tools for rasam drawings. */
2
+ export { Snapper } from './snapping/snapper.js';
3
+ //# sourceMappingURL=snapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapping.js","sourceRoot":"","sources":["../src/snapping.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@rasamjs/tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Opt-in editing tools for rasam SVG drawings.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/hdevlop/rasam.git",
10
+ "directory": "packages/tools"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "README.md",
16
+ "API.md",
17
+ "CHANGELOG.md",
18
+ "RELEASING.md",
19
+ "LICENSE"
20
+ ],
21
+ "exports": {
22
+ "./snapping": {
23
+ "rasam-source": "./src/snapping.ts",
24
+ "types": "./dist/snapping.d.ts",
25
+ "import": "./dist/snapping.js"
26
+ },
27
+ "./snapping/advanced": {
28
+ "rasam-source": "./src/snapping/advanced.ts",
29
+ "types": "./dist/snapping/advanced.d.ts",
30
+ "import": "./dist/snapping/advanced.js"
31
+ },
32
+ "./grid": {
33
+ "rasam-source": "./src/grid.ts",
34
+ "types": "./dist/grid.d.ts",
35
+ "import": "./dist/grid.js"
36
+ },
37
+ "./rulers": {
38
+ "rasam-source": "./src/rulers.ts",
39
+ "types": "./dist/rulers.d.ts",
40
+ "import": "./dist/rulers.js"
41
+ },
42
+ "./drawing": {
43
+ "rasam-source": "./src/drawing.ts",
44
+ "types": "./dist/drawing.d.ts",
45
+ "import": "./dist/drawing.js"
46
+ }
47
+ },
48
+ "sideEffects": false,
49
+ "peerDependencies": {
50
+ "@rasamjs/core": "^0.1.0"
51
+ },
52
+ "devDependencies": {
53
+ "@rasamjs/core": "0.1.2",
54
+ "@rasamjs/history": "0.1.0",
55
+ "@rasamjs/selection": "0.3.0",
56
+ "@rasamjs/transformer": "0.4.0",
57
+ "@rasamjs/viewport": "0.1.0"
58
+ },
59
+ "scripts": {
60
+ "build": "node scripts/build.mjs",
61
+ "typecheck": "tsc -p tsconfig.check.json",
62
+ "test": "vitest run",
63
+ "test:browser": "playwright test",
64
+ "dev": "vite --host 127.0.0.1",
65
+ "format": "prettier --write src tests scripts fixtures examples *.json *.ts *.md",
66
+ "format:check": "prettier --check src tests scripts fixtures examples *.json *.ts *.md",
67
+ "check:consumer": "node scripts/check-consumer.mjs",
68
+ "check:docs": "node scripts/check-docs.mjs",
69
+ "check:size": "node scripts/check-size.mjs",
70
+ "release:check": "node scripts/check-release.mjs",
71
+ "api:check": "node scripts/check-api.mjs",
72
+ "api:update": "node scripts/check-api.mjs --update",
73
+ "bench:drag": "node scripts/benchmark-drag.mjs"
74
+ }
75
+ }
@@ -0,0 +1,15 @@
1
+ import type { Svg } from '@rasamjs/core';
2
+ import type { ResolvedOptions } from '../snapping/options.js';
3
+ import type { GuideOverlay } from '../snapping/overlay.js';
4
+ import type { AdvancedSnapperOptions } from './types.js';
5
+
6
+ export interface SessionContext<Request> {
7
+ readonly drawing: Svg;
8
+ readonly options: ResolvedOptions;
9
+ readonly advanced: AdvancedSnapperOptions;
10
+ readonly overlay: GuideOverlay;
11
+ readonly request: Request;
12
+ readonly isEnabled: () => boolean;
13
+ readonly isCurrent: () => boolean;
14
+ readonly onEnd: () => void;
15
+ }
@@ -0,0 +1,65 @@
1
+ import { PathArray } from '@rasamjs/core';
2
+ import type { AnyNode, Svg, PathCommand } from '@rasamjs/core';
3
+ import { Guards } from '@rasamjs/core/advanced';
4
+ import { clientMatrix } from './geometry.js';
5
+ import type { Segment } from './geometry.js';
6
+
7
+ function subpaths(geometry: SVGGeometryElement): SVGGeometryElement[] {
8
+ if (geometry.localName !== 'path') return [geometry];
9
+ const parts: PathCommand[][] = [];
10
+ for (const command of new PathArray(geometry.getAttribute('d') ?? '').toArray()) {
11
+ if (command[0] === 'M') parts.push([]);
12
+ parts.at(-1)!.push(command);
13
+ }
14
+ return parts.map((commands) => {
15
+ const path = geometry.cloneNode(false) as SVGGeometryElement;
16
+ path.setAttribute('d', new PathArray(commands).toString());
17
+ return path;
18
+ });
19
+ }
20
+
21
+ /** Detached geometry is sampled once: target edits never change an active session. */
22
+ export function curveSegments(
23
+ drawing: Svg,
24
+ nodes: readonly AnyNode[],
25
+ resolution: number,
26
+ ): Segment[] {
27
+ const root = drawing.worldMatrix().inverse(),
28
+ screen = clientMatrix(drawing);
29
+ if (!root || !screen) return [];
30
+ const segments: Segment[] = [];
31
+ for (const node of nodes) {
32
+ if (!Guards.isTransformable(node)) continue;
33
+ if (!['path', 'line', 'polyline', 'polygon', 'circle', 'ellipse'].includes(node.el.localName))
34
+ continue;
35
+ node.flush();
36
+ const geometry = node.el.cloneNode(false) as SVGGeometryElement;
37
+ if (typeof geometry.getTotalLength !== 'function') continue;
38
+ const matrix = root.multiply(node.worldMatrix());
39
+ const pixels = screen.multiply(matrix);
40
+ const stretch = Math.hypot(pixels.a, pixels.b, pixels.c, pixels.d);
41
+ let parts: SVGGeometryElement[];
42
+ try {
43
+ parts = subpaths(geometry);
44
+ } catch {
45
+ continue;
46
+ }
47
+ for (const part of parts) {
48
+ let length: number;
49
+ try {
50
+ length = part.getTotalLength();
51
+ } catch {
52
+ continue;
53
+ }
54
+ if (!Number.isFinite(length) || length <= 0) continue;
55
+ const count = Math.max(1, Math.min(8192, Math.ceil((length * stretch) / resolution)));
56
+ let previous = matrix.apply(part.getPointAtLength(0));
57
+ for (let index = 1; index <= count; index++) {
58
+ const next = matrix.apply(part.getPointAtLength((length * index) / count));
59
+ segments.push([previous, next]);
60
+ previous = next;
61
+ }
62
+ }
63
+ }
64
+ return segments;
65
+ }
@@ -0,0 +1,74 @@
1
+ import { Matrix } from '@rasamjs/core';
2
+ import type { Point, Svg } from '@rasamjs/core';
3
+
4
+ export type Segment = readonly [Point, Point];
5
+ export const add = (a: Point, b: Point): Point => ({ x: a.x + b.x, y: a.y + b.y });
6
+ export const subtract = (a: Point, b: Point): Point => ({ x: a.x - b.x, y: a.y - b.y });
7
+ export const scale = (p: Point, value: number): Point => ({ x: p.x * value, y: p.y * value });
8
+ const cross = (a: Point, b: Point) => a.x * b.y - a.y * b.x;
9
+
10
+ /** The closest point on cached segments, optionally restricted to a resize line. */
11
+ export function nearestOnSegments(
12
+ point: Point,
13
+ segments: readonly Segment[],
14
+ direction?: Point,
15
+ ): Point | null {
16
+ let best: Point | null = null,
17
+ distance = Infinity;
18
+ for (const [a, b] of segments) {
19
+ const edge = subtract(b, a),
20
+ relative = subtract(a, point);
21
+ let candidate: Point;
22
+ const determinant = direction && cross(direction, edge);
23
+ if (direction && Math.abs(determinant!) > 1e-12) {
24
+ const along = cross(relative, direction) / determinant!;
25
+ if (along < 0 || along > 1) continue;
26
+ candidate = add(point, scale(direction, cross(relative, edge) / determinant!));
27
+ } else {
28
+ if (direction && Math.abs(cross(relative, direction)) > 1e-9) continue;
29
+ const length = edge.x * edge.x + edge.y * edge.y;
30
+ const t = length
31
+ ? Math.max(0, Math.min(1, ((point.x - a.x) * edge.x + (point.y - a.y) * edge.y) / length))
32
+ : 0;
33
+ candidate = add(a, scale(edge, t));
34
+ }
35
+ const next = Math.hypot(candidate.x - point.x, candidate.y - point.y);
36
+ if (next < distance) {
37
+ best = candidate;
38
+ distance = next;
39
+ }
40
+ }
41
+ return best;
42
+ }
43
+
44
+ /** Root-to-client mapping is read once per sample, never once per target segment. */
45
+ export function clientMatrix(drawing: Svg): Matrix | null {
46
+ const a = drawing.toClient({ x: 0, y: 0 });
47
+ const b = drawing.toClient({ x: 1, y: 0 });
48
+ const c = drawing.toClient({ x: 0, y: 1 });
49
+ return a && b && c ? new Matrix(b.x - a.x, b.y - a.y, c.x - a.x, c.y - a.y, a.x, a.y) : null;
50
+ }
51
+
52
+ export function frameConstraint(corners: readonly Point[], anchor: Point) {
53
+ const [a, b, , d] = corners;
54
+ const u = subtract(b, a),
55
+ v = subtract(d, a);
56
+ const inverse = new Matrix(u.x, u.y, v.x, v.y, 0, 0).inverse();
57
+ if (!inverse) return null;
58
+ const point = add(a, add(scale(u, anchor.x), scale(v, anchor.y)));
59
+ const direction = anchor.x === 0.5 ? v : anchor.y === 0.5 ? u : undefined;
60
+ return {
61
+ point,
62
+ direction,
63
+ project(delta: Point): Point {
64
+ const unit = inverse.apply(delta);
65
+ return add(
66
+ point,
67
+ add(scale(u, anchor.x === 0.5 ? 0 : unit.x), scale(v, anchor.y === 0.5 ? 0 : unit.y)),
68
+ );
69
+ },
70
+ };
71
+ }
72
+
73
+ export const angleDifference = (a: number, b: number, period = 360): number =>
74
+ ((((a - b + period * 1.5) % period) + period) % period) - period / 2;
@@ -0,0 +1,93 @@
1
+ import { Matrix } from '@rasamjs/core';
2
+ import type { Point } from '@rasamjs/core';
3
+ import { Guards } from '@rasamjs/core/advanced';
4
+ import { assertPoint } from '../snapping/options.js';
5
+ import { targetNodes, validateNodes } from '../snapping/targets.js';
6
+ import { add, subtract, angleDifference } from './geometry.js';
7
+ import type { SessionContext } from './context.js';
8
+ import type { RotationSnapRequest, RotationSnapSession } from './types.js';
9
+
10
+ export function createRotationSession(
11
+ context: SessionContext<RotationSnapRequest>,
12
+ ): RotationSnapSession | null {
13
+ const { request } = context;
14
+ validateNodes(request.nodes, context.drawing);
15
+ assertPoint(request.client);
16
+ assertPoint(request.pivot);
17
+ if (request.corners.length !== 4) throw new TypeError('Rotation needs four corners');
18
+ const root = context.drawing.worldMatrix().inverse();
19
+ const pivot = context.drawing.fromClient(request.pivot),
20
+ start = context.drawing.fromClient(request.client);
21
+ const corners = request.corners.map((p) => context.drawing.fromClient(p));
22
+ if (!root || !pivot || !start || corners.some((p) => !p)) return null;
23
+ const parent =
24
+ request.nodes.length === 1 && Guards.isTransformable(request.nodes[0].parent)
25
+ ? root.multiply(request.nodes[0].parent.worldMatrix())
26
+ : Matrix.identity();
27
+ const inverse = parent.inverse();
28
+ if (!inverse) return null;
29
+ const vector = (p: Point) => subtract(inverse.apply(add(p, pivot)), inverse.apply(pivot));
30
+ const first = vector(subtract(corners[1]!, corners[0]!));
31
+ const startAngle = Math.atan2(vector(subtract(start, pivot)).y, vector(subtract(start, pivot)).x);
32
+ const frameAngle = Math.atan2(first.y, first.x);
33
+ const angles = targetNodes(context.drawing, request.nodes, context.options).flatMap((node) => {
34
+ if (!Guards.isTransformable(node)) return [];
35
+ const matrix = root.multiply(node.worldMatrix());
36
+ return [Math.atan2(matrix.b, matrix.a)];
37
+ });
38
+ if (!angles.length || !context.isCurrent() || !context.isEnabled()) return null;
39
+ let ended = false,
40
+ snapped = false;
41
+ const session: RotationSnapSession = {
42
+ get angleSnapped() {
43
+ return snapped;
44
+ },
45
+ snap: (client, modifiers) => {
46
+ assertPoint(client);
47
+ snapped = false;
48
+ if (
49
+ ended ||
50
+ !context.isEnabled() ||
51
+ (modifiers.ignore && context.options.modifier !== 'none')
52
+ )
53
+ return client;
54
+ const current = context.drawing.fromClient(client);
55
+ if (!current) return client;
56
+ const local = vector(subtract(current, pivot));
57
+ const proposed = frameAngle + Math.atan2(local.y, local.x) - startAngle;
58
+ const mapped = subtract(
59
+ parent.apply({ x: Math.cos(proposed), y: Math.sin(proposed) }),
60
+ parent.apply({ x: 0, y: 0 }),
61
+ );
62
+ const visual = Math.atan2(mapped.y, mapped.x);
63
+ let best: number | null = null,
64
+ distance = ((context.advanced.angleTolerance ?? 5) * Math.PI) / 180;
65
+ for (const target of angles) {
66
+ const next = Math.abs(angleDifference(target, visual, Math.PI));
67
+ if (next <= distance) {
68
+ distance = next;
69
+ best = target;
70
+ }
71
+ }
72
+ if (best === null) return client;
73
+ const target = vector({ x: Math.cos(best), y: Math.sin(best) });
74
+ const correction = angleDifference(Math.atan2(target.y, target.x), proposed, Math.PI);
75
+ const rotated = {
76
+ x: local.x * Math.cos(correction) - local.y * Math.sin(correction),
77
+ y: local.x * Math.sin(correction) + local.y * Math.cos(correction),
78
+ };
79
+ const offset = subtract(parent.apply(rotated), parent.apply({ x: 0, y: 0 }));
80
+ const result = context.drawing.toClient(add(pivot, offset));
81
+ snapped = !!result;
82
+ return result ?? client;
83
+ },
84
+ end: () => {
85
+ if (!ended) {
86
+ ended = true;
87
+ snapped = false;
88
+ context.onEnd();
89
+ }
90
+ },
91
+ };
92
+ return session;
93
+ }