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.
- package/README.md +170 -0
- package/lab/kish-test.js +35 -0
- package/lab/kitest-org/kitest-backups/kitest-2026-02-03_180334.zip +0 -0
- package/lab/kitest-org/kitest.kicad_pcb +2 -0
- package/lab/kitest-org/kitest.kicad_prl +98 -0
- package/lab/kitest-org/kitest.kicad_pro +418 -0
- package/lab/kitest-org/kitest.kicad_sch +663 -0
- package/package.json +22 -0
- package/spec/Entity.spec.js +31 -0
- package/spec/Schematic.spec.js +115 -0
- package/spec/SymbolLibrary.spec.js +17 -0
- package/spec/cartesian-math.spec.js +25 -0
- package/spec/kitest.kicad_sch +663 -0
- package/spec/manhattan-router.spec.js +152 -0
- package/spec/sexp.spec.js +14 -0
- package/spec/support/jasmine.mjs +14 -0
- package/src/Entity.js +232 -0
- package/src/LibrarySymbol.js +100 -0
- package/src/Schematic.js +329 -0
- package/src/SymbolLibrary.js +101 -0
- package/src/cartesian-math.js +127 -0
- package/src/kisch-cli.js +33 -0
- package/src/manhattan-router.js +176 -0
- package/src/place-rect.js +45 -0
- package/src/sexp.js +178 -0
|
@@ -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
|
+
}
|