kisch 1.0.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.
@@ -0,0 +1,176 @@
1
+ import { Point, pointKey } from "../src/cartesian-math.js";
2
+
3
+ function manhattan(a, b) {
4
+ return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
5
+ }
6
+
7
+ function rangeOverlap(a1, a2, b1, b2) {
8
+ const minA = Math.min(a1, a2);
9
+ const maxA = Math.max(a1, a2);
10
+ const minB = Math.min(b1, b2);
11
+ const maxB = Math.max(b1, b2);
12
+ return maxA > minB && maxB > minA;
13
+ }
14
+
15
+ /* ───────────────────────── Rect blocking (unchanged) ───────────────────────── */
16
+
17
+ export function segmentBlocked(a, b, rects) {
18
+ const ax = a[0], ay = a[1];
19
+ const bx = b[0], by = b[1];
20
+
21
+ for (const r of rects) {
22
+ const rx = r.corner[0];
23
+ const ry = r.corner[1];
24
+ const rw = r.size[0];
25
+ const rh = r.size[1];
26
+
27
+ // horizontal
28
+ if (ay === by) {
29
+ if (
30
+ ay >= ry && ay <= ry + rh &&
31
+ rangeOverlap(ax, bx, rx, rx + rw)
32
+ ) return true;
33
+ }
34
+
35
+ // vertical
36
+ if (ax === bx) {
37
+ if (
38
+ ax >= rx && ax <= rx + rw &&
39
+ rangeOverlap(ay, by, ry, ry + rh)
40
+ ) return true;
41
+ }
42
+ }
43
+
44
+ return false;
45
+ }
46
+
47
+ /* ───────────────────────── Line blocking (NEW) ───────────────────────── */
48
+
49
+ function similar(a,b) {
50
+ return Math.abs(a-b)<0.0000001
51
+ }
52
+
53
+ function segmentOverlapsLine(a1, a2, b1, b2) {
54
+ // horizontal
55
+ // if (a1[1] === a2[1] && b1[1] === b2[1] && a1[1] === b1[1]) {
56
+ if (similar(a1[1],a2[1]) && similar(b1[1],b2[1]) && similar(a1[1],b1[1])) {
57
+ return rangeOverlap(a1[0], a2[0], b1[0], b2[0]);
58
+ }
59
+
60
+ // vertical
61
+ // if (a1[0] === a2[0] && b1[0] === b2[0] && a1[0] === b1[0]) {
62
+ if (similar(a1[0],a2[0]) && similar(b1[0],b2[0]) && similar(a1[0],b1[0])) {
63
+ return rangeOverlap(a1[1], a2[1], b1[1], b2[1]);
64
+ }
65
+
66
+ return false;
67
+ }
68
+
69
+ export function segmentBlockedByLines(a, b, lines) {
70
+ for (const { a: l1, b: l2 } of lines) {
71
+ if (segmentOverlapsLine(a, b, l1, l2)) {
72
+ return true;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+
78
+ /* ───────────────────────── Path compression (unchanged) ───────────────────────── */
79
+
80
+ function compressPath(points) {
81
+ if (points.length <= 2) return points;
82
+
83
+ const out = [points[0]];
84
+
85
+ for (let i = 1; i < points.length - 1; i++) {
86
+ const a = out[out.length - 1];
87
+ const b = points[i];
88
+ const c = points[i + 1];
89
+
90
+ const abx = b[0] - a[0];
91
+ const aby = b[1] - a[1];
92
+ const bcx = c[0] - b[0];
93
+ const bcy = c[1] - b[1];
94
+
95
+ if (
96
+ (abx === 0 && bcx === 0) ||
97
+ (aby === 0 && bcy === 0)
98
+ ) continue;
99
+
100
+ out.push(b);
101
+ }
102
+
103
+ out.push(points[points.length - 1]);
104
+ return out; //.map(p=>p.snap(2.54));
105
+ }
106
+
107
+ /* ───────────────────────── Main router ───────────────────────── */
108
+
109
+ export function findGridPath({
110
+ from,
111
+ to,
112
+ gridSize,
113
+ avoidRects = [],
114
+ avoidLines = []
115
+ }) {
116
+ const open = new Map();
117
+ const closed = new Set();
118
+
119
+ open.set(pointKey(from), {
120
+ point: from,
121
+ g: 0,
122
+ f: manhattan(from, to),
123
+ parent: null
124
+ });
125
+
126
+ const steps = [
127
+ new Point([ gridSize, 0 ]),
128
+ new Point([ -gridSize, 0 ]),
129
+ new Point([ 0, gridSize ]),
130
+ new Point([ 0, -gridSize ])
131
+ ];
132
+
133
+ while (open.size) {
134
+ let current = null;
135
+ for (const n of open.values()) {
136
+ if (!current || n.f < current.f) current = n;
137
+ }
138
+
139
+ if (current.point.equals(to)) {
140
+ const path = [];
141
+ let c = current;
142
+ while (c) {
143
+ path.push(c.point);
144
+ c = c.parent;
145
+ }
146
+ return compressPath(path.reverse());
147
+ }
148
+
149
+ open.delete(pointKey(current.point));
150
+ closed.add(pointKey(current.point));
151
+
152
+ for (const step of steps) {
153
+ const next = current.point.add(step);
154
+ const key = pointKey(next);
155
+
156
+ if (closed.has(key)) continue;
157
+ if (segmentBlocked(current.point, next, avoidRects)) continue;
158
+ if (segmentBlockedByLines(current.point, next, avoidLines)) continue;
159
+
160
+ const g = current.g + gridSize;
161
+ const f = g + manhattan(next, to);
162
+
163
+ const existing = open.get(key);
164
+ if (!existing || g < existing.g) {
165
+ open.set(key, {
166
+ point: next,
167
+ g,
168
+ f,
169
+ parent: current
170
+ });
171
+ }
172
+ }
173
+ }
174
+
175
+ throw new Error("No path found");
176
+ }
@@ -0,0 +1,45 @@
1
+ export function placeRect({ rect, start, step, avoid, maxRadius = 1000 }) {
2
+ // test a candidate anchor point
3
+ function fitsAt(p) {
4
+ const placed = rect.translate(p);
5
+ return !avoid.some(r => placed.overlaps(r));
6
+ }
7
+
8
+ // try start position first
9
+ if (fitsAt(start)) {
10
+ return start;
11
+ }
12
+
13
+ let x = 0;
14
+ let y = 0;
15
+ let dir = 0;
16
+ let leg = 1;
17
+
18
+ const dirs = [
19
+ [ 1, 0], // right
20
+ [ 0, 1], // down
21
+ [-1, 0], // left
22
+ [ 0, -1], // up
23
+ ];
24
+
25
+ while (leg <= maxRadius) {
26
+ for (let repeat = 0; repeat < 2; repeat++) {
27
+ const [dx, dy] = dirs[dir & 3];
28
+
29
+ for (let i = 0; i < leg; i++) {
30
+ x += dx;
31
+ y += dy;
32
+
33
+ const p = start.add([x * step, y * step]);
34
+ if (fitsAt(p)) {
35
+ return p;
36
+ }
37
+ }
38
+
39
+ dir++;
40
+ }
41
+ leg++;
42
+ }
43
+
44
+ return null;
45
+ }
package/src/sexp.js ADDED
@@ -0,0 +1,178 @@
1
+ import util from 'node:util';
2
+
3
+ /*export function sexpFind(sexp, pred) {
4
+ if (!Array.isArray(sexp))
5
+ return;
6
+
7
+
8
+ }*/
9
+
10
+ export function sexpCallName(sexp) {
11
+ if (!Array.isArray(sexp))
12
+ return;
13
+
14
+ return symName(sexp[0]);
15
+ }
16
+
17
+ export function sym(name) {
18
+ return { $sym: String(name) }
19
+ }
20
+
21
+ export function isSym(v) {
22
+ return (
23
+ v !== null &&
24
+ typeof v === 'object' &&
25
+ typeof v.$sym === 'string'
26
+ )
27
+ }
28
+
29
+ export function symName(v) {
30
+ return isSym(v) ? v.$sym : null
31
+ }
32
+
33
+ export function symEq(v, other) {
34
+ if (!isSym(v)) return false
35
+
36
+ if (isSym(other)) {
37
+ return v.$sym === other.$sym
38
+ }
39
+
40
+ if (typeof other === 'string') {
41
+ return v.$sym === other
42
+ }
43
+
44
+ return false
45
+ }
46
+
47
+ export function sexpParse(input) {
48
+ let i = 0
49
+
50
+ const skipWhitespace = () => {
51
+ while (i < input.length && /\s/.test(input[i])) i++
52
+ }
53
+
54
+ const parseString = () => {
55
+ let str = ''
56
+ i++ // skip opening "
57
+ while (i < input.length) {
58
+ if (input[i] === '"') {
59
+ i++
60
+ break
61
+ } else if (input[i] === '\\') {
62
+ i++
63
+ str += input[i] || ''
64
+ } else {
65
+ str += input[i]
66
+ }
67
+ i++
68
+ }
69
+ return str
70
+ }
71
+
72
+ const parseAtomOrNumber = () => {
73
+ let start = i
74
+ while (i < input.length && /[^\s()]/.test(input[i])) i++
75
+ const token = input.slice(start, i)
76
+
77
+ if (!isNaN(token)) {
78
+ return Number(token)
79
+ }
80
+
81
+ return sym(token)
82
+ }
83
+
84
+ const parseList = () => {
85
+ const list = []
86
+ i++ // skip '('
87
+ while (i < input.length) {
88
+ skipWhitespace()
89
+ if (i >= input.length) break
90
+
91
+ if (input[i] === ')') {
92
+ i++
93
+ break
94
+ } else if (input[i] === '(') {
95
+ list.push(parseList())
96
+ } else if (input[i] === '"') {
97
+ list.push(parseString())
98
+ } else {
99
+ list.push(parseAtomOrNumber())
100
+ }
101
+ }
102
+ return list
103
+ }
104
+
105
+ const result = []
106
+ while (i < input.length) {
107
+ skipWhitespace()
108
+ if (i >= input.length) break
109
+
110
+ if (input[i] === '(') {
111
+ result.push(parseList())
112
+ } else if (input[i] === '"') {
113
+ result.push(parseString())
114
+ } else {
115
+ result.push(parseAtomOrNumber())
116
+ }
117
+ }
118
+
119
+ return result
120
+ }
121
+
122
+ export function sexpStringify(sexpr, indent) {
123
+ const pretty = typeof indent === 'number' && indent > 0
124
+ const space = (n) => pretty ? ' '.repeat(indent * n) : ''
125
+
126
+ const serializeAtom = (node) => {
127
+ if (isSym(node)) {
128
+ return symName(node)
129
+ }
130
+ if (typeof node === 'string') {
131
+ return `"${node.replace(/(["\\])/g, '\\$1')}"`
132
+ }
133
+ return String(node)
134
+ }
135
+
136
+ const serialize = (node, level) => {
137
+ // atom
138
+ if (!Array.isArray(node)) {
139
+ return serializeAtom(node)
140
+ }
141
+
142
+ if (node.length === 0) return '()'
143
+
144
+ const head = node[0]
145
+ const rest = node.slice(1)
146
+
147
+ let out = '(' + serializeAtom(head)
148
+ let i = 0
149
+
150
+ // inline atoms after head
151
+ while (i < rest.length && !Array.isArray(rest[i])) {
152
+ out += ' ' + serializeAtom(rest[i])
153
+ i++
154
+ }
155
+
156
+ // no pretty-printing or everything fit inline
157
+ if (!pretty || i === rest.length) {
158
+ for (; i < rest.length; i++) {
159
+ out += ' ' + serialize(rest[i], level + 1)
160
+ }
161
+ return out + ')'
162
+ }
163
+
164
+ // pretty-print remaining forms
165
+ const lines = rest.slice(i).map(
166
+ n => space(level + 1) + serialize(n, level + 1)
167
+ )
168
+
169
+ return (
170
+ out + '\n' +
171
+ lines.join('\n') +
172
+ '\n' +
173
+ space(level) + ')'
174
+ )
175
+ }
176
+
177
+ return sexpr.map(node => serialize(node, 0)).join(pretty ? '\n' : ' ')
178
+ }