hanzi-chai 0.3.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 (52) hide show
  1. package/dist/affine.d.ts +8 -0
  2. package/dist/affine.js +153 -0
  3. package/dist/assembly.d.ts +63 -0
  4. package/dist/assembly.js +286 -0
  5. package/dist/bezier.d.ts +74 -0
  6. package/dist/bezier.js +140 -0
  7. package/dist/classifier.d.ts +109 -0
  8. package/dist/classifier.js +108 -0
  9. package/dist/component.d.ts +115 -0
  10. package/dist/component.js +278 -0
  11. package/dist/compound.d.ts +64 -0
  12. package/dist/compound.js +652 -0
  13. package/dist/config.d.ts +236 -0
  14. package/dist/config.js +48 -0
  15. package/dist/data/cjk.txt +20902 -0
  16. package/dist/data/data/cjk.txt +20902 -0
  17. package/dist/data/data/dictionary.txt +69429 -0
  18. package/dist/data/data/frequency.txt +263137 -0
  19. package/dist/data/data/key_distribution.txt +46 -0
  20. package/dist/data/data/pair_equivalence.txt +2116 -0
  21. package/dist/data/data/repertoire.json.deflate +0 -0
  22. package/dist/data/data/tygf.txt +8105 -0
  23. package/dist/data/dictionary.txt +69429 -0
  24. package/dist/data/frequency.txt +263137 -0
  25. package/dist/data/key_distribution.txt +46 -0
  26. package/dist/data/pair_equivalence.txt +2116 -0
  27. package/dist/data/repertoire.json.deflate +0 -0
  28. package/dist/data/tygf.txt +8105 -0
  29. package/dist/data.d.ts +195 -0
  30. package/dist/data.js +18 -0
  31. package/dist/degenerator.d.ts +21 -0
  32. package/dist/degenerator.js +134 -0
  33. package/dist/element.d.ts +56 -0
  34. package/dist/element.js +225 -0
  35. package/dist/equivalence.d.ts +21 -0
  36. package/dist/equivalence.js +74 -0
  37. package/dist/index.d.ts +45 -0
  38. package/dist/index.js +126 -0
  39. package/dist/index.mjs +3553 -0
  40. package/dist/mathjs.d.ts +10 -0
  41. package/dist/mathjs.js +9 -0
  42. package/dist/repertoire.d.ts +75 -0
  43. package/dist/repertoire.js +406 -0
  44. package/dist/selector.d.ts +110 -0
  45. package/dist/selector.js +310 -0
  46. package/dist/topology.d.ts +41 -0
  47. package/dist/topology.js +209 -0
  48. package/dist/transformer.d.ts +18 -0
  49. package/dist/transformer.js +111 -0
  50. package/dist/utils.d.ts +107 -0
  51. package/dist/utils.js +546 -0
  52. package/package.json +34 -0
@@ -0,0 +1,310 @@
1
+ import { NoSchemeError, } from "./component.js";
2
+ import { isMerge } from "./config.js";
3
+ import { binaryToIndices } from "./degenerator.js";
4
+ import { isEqual } from "lodash-es";
5
+ import { sortTwoNumbers } from "./bezier.js";
6
+ export const defaultSelector = [
7
+ "结构完整",
8
+ "根少优先",
9
+ "能连不交",
10
+ "能散不连",
11
+ "全符笔顺",
12
+ "取大优先",
13
+ ];
14
+ export function isLess(a, b) {
15
+ if (typeof a === "number" && typeof b === "number") {
16
+ return a < b;
17
+ }
18
+ if (Array.isArray(a) && Array.isArray(b)) {
19
+ for (const [i, v] of a.entries()) {
20
+ const u = b[i];
21
+ if (u === undefined)
22
+ return false;
23
+ if (v < u)
24
+ return true;
25
+ if (v > u)
26
+ return false;
27
+ }
28
+ return false;
29
+ }
30
+ return false;
31
+ }
32
+ export const length = {
33
+ title: "根少优先",
34
+ key: (scheme) => scheme.length,
35
+ };
36
+ const countStrokes = (n) => n < 2 ? n : (n & 1) + countStrokes(n >>> 1);
37
+ /**
38
+ * 规则:取大优先
39
+ *
40
+ * 让顺序靠前的字根尽量取到更多的笔画
41
+ */
42
+ export const bias = {
43
+ title: "取大优先",
44
+ key: (scheme) => scheme.map(countStrokes).map((x) => -x),
45
+ display: (data) => `(${data.map((x) => -x).join(", ")})`,
46
+ };
47
+ /**
48
+ * 规则:取小优先
49
+ *
50
+ * 让顺序靠前的字根尽量取到更少的笔画
51
+ */
52
+ export const unbias = {
53
+ title: "取小优先",
54
+ key: (scheme) => scheme.map(countStrokes),
55
+ display: (data) => `(${data.join(", ")})`,
56
+ };
57
+ /**
58
+ * 规则:全符笔顺
59
+ *
60
+ * 各个字根中笔画的顺序连在一起是否与整个部件的笔顺相同,相同者优先
61
+ * 该规则采集自三码郑码的文档
62
+ *
63
+ * @see https://www.yuque.com/smzm/zhengma/otb32d
64
+ */
65
+ export const order = {
66
+ title: "全符笔顺",
67
+ key: (scheme, { component }) => {
68
+ const indices = scheme.flatMap((x) => binaryToIndices(component.glyph.length)(x));
69
+ const isSorted = indices.slice(1).every((v, i) => indices[i] < v);
70
+ return Number(!isSorted);
71
+ },
72
+ };
73
+ /**
74
+ * 规则:连续笔顺
75
+ *
76
+ * 字根是否由部件中连续的几个笔画构成,不符合此特点的字根数量少者优先。该规则采集自宇浩输入法的文档。
77
+ *
78
+ * @see https://zhuyuhao.com/yuhao/docs/learn.html#符合笔顺
79
+ */
80
+ export const order2 = {
81
+ title: "连续笔顺",
82
+ key: (scheme, { component }) => {
83
+ const indices = scheme.map(binaryToIndices(component.glyph.length));
84
+ // 如果一个字根不是由连续的笔画构成,那么称它是不连续的
85
+ const unSorted = indices.filter((x) => x.length - 1 !== x.at(-1) - x[0]).length;
86
+ // 让不连续的字根数量少者优先
87
+ return unSorted;
88
+ },
89
+ };
90
+ /**
91
+ * 规则:非形近根
92
+ *
93
+ * 尽量少使用被归并到其他字根的字根。该规则采集自三码郑码的文档
94
+ *
95
+ * @see https://www.yuque.com/smzm/zhengma/otb32d
96
+ */
97
+ export const similar = {
98
+ title: "非形近根",
99
+ key: (scheme, { rootMap, roots }) => {
100
+ const rootList = scheme.map((x) => rootMap.get(x));
101
+ return rootList.filter((x) => {
102
+ const value = roots.get(x);
103
+ return value && isMerge(value);
104
+ }).length;
105
+ },
106
+ };
107
+ /**
108
+ * 规则:强字根
109
+ *
110
+ * 尽量多使用特定的一些字根。该规则采集自郑码的文档
111
+ */
112
+ export const strong = {
113
+ title: "多强字根",
114
+ key: (scheme, { rootMap, analysis }) => {
115
+ const strong = analysis?.strong || [];
116
+ const roots = scheme.map((x) => rootMap.get(x));
117
+ return -roots.filter((x) => strong.includes(x)).length;
118
+ },
119
+ };
120
+ /**
121
+ * 规则:弱字根
122
+ *
123
+ * 尽量避免使用特定的一些字根
124
+ */
125
+ export const weak = {
126
+ title: "少弱字根",
127
+ key: (scheme, { rootMap, analysis }) => {
128
+ const weak = analysis?.weak || [];
129
+ const roots = scheme.map((x) => rootMap.get(x));
130
+ return roots.filter((x) => weak.includes(x)).length;
131
+ },
132
+ };
133
+ const makeTopologySieve = (relationType, avoidRelationType, title) => {
134
+ const key = (scheme, { component }) => {
135
+ const parsedScheme = scheme.map((x) => binaryToIndices(component.glyph.length)(x));
136
+ let totalCrosses = 0;
137
+ for (const [i, bi] of parsedScheme.entries()) {
138
+ for (const [j, bj] of parsedScheme.entries()) {
139
+ if (j >= i)
140
+ continue;
141
+ let r = false;
142
+ let a = false;
143
+ for (const k of bi) {
144
+ for (const l of bj) {
145
+ const [smaller, larger] = [Math.min(k, l), Math.max(k, l)];
146
+ const relations = component.topology.matrix[larger]?.[smaller];
147
+ r ||= relations.some((v) => v.type === relationType);
148
+ a ||= relations.some((v) => avoidRelationType.includes(v.type));
149
+ }
150
+ }
151
+ if (!a)
152
+ totalCrosses += Number(r);
153
+ }
154
+ }
155
+ return totalCrosses;
156
+ };
157
+ return { key, title };
158
+ };
159
+ /**
160
+ * 规则:能连不交
161
+ *
162
+ * 尽量让字根不交叉。该规则采集自五笔
163
+ */
164
+ export const crossing = makeTopologySieve("交", [], "能连不交");
165
+ /**
166
+ * 规则:能散不连
167
+ *
168
+ * 尽量让字根不相连。该规则采集自五笔
169
+ */
170
+ export const attaching = makeTopologySieve("连", ["交"], "能散不连");
171
+ /**
172
+ * 规则:同向笔画
173
+ *
174
+ * 尽量让方向相同的笔画包含在同一个字根里。该规则采集自五笔
175
+ */
176
+ export const orientation = {
177
+ title: "同向笔画",
178
+ key: (scheme, { component }) => {
179
+ const n = component.glyph.length;
180
+ const parsedScheme = scheme.map(binaryToIndices(n));
181
+ let totalCrosses = 0;
182
+ for (const [i, bi] of parsedScheme.entries()) {
183
+ for (const [j, bj] of parsedScheme.entries()) {
184
+ if (j >= i)
185
+ continue;
186
+ let r = false;
187
+ for (const k of bi) {
188
+ for (const l of bj) {
189
+ const [smaller, larger] = sortTwoNumbers([k, l]);
190
+ const oriented = component.topology.orientedPairs.some((x) => isEqual(x, [larger, smaller]));
191
+ r ||= oriented;
192
+ }
193
+ }
194
+ totalCrosses += Number(r);
195
+ }
196
+ }
197
+ return totalCrosses;
198
+ },
199
+ };
200
+ /**
201
+ * @param b1 - 以二进制数表示的切片
202
+ * @param b2 - 同上
203
+ * @returns 第一个切片是否包含第二个切片
204
+ */
205
+ const contains = (b1, b2) => (b1 | b2) === b1;
206
+ /**
207
+ * 规则:结构完整
208
+ *
209
+ * 避免框类部件被拆散。该规则采集自宇浩输入法的文档
210
+ *
211
+ * @see https://zhuyuhao.com/yuhao/docs/learn.html#结构完整
212
+ */
213
+ export const integrity = {
214
+ title: "结构完整",
215
+ key: (scheme, { rootMap }) => {
216
+ const priorities = [
217
+ "口",
218
+ "囗",
219
+ "冂",
220
+ "\ue439" /* 见二 */,
221
+ "匚",
222
+ "凵",
223
+ "\ue009" /* 假右角 */,
224
+ "勹",
225
+ "尸",
226
+ "\uE407" /* 央三 */,
227
+ ];
228
+ const shouldHave = [...rootMap]
229
+ .filter(([_, name]) => priorities.includes(name))
230
+ .map(([binary]) => binary);
231
+ const breaks = shouldHave.filter((x) => !scheme.some((binary) => contains(binary, x))).length;
232
+ return breaks;
233
+ },
234
+ };
235
+ export const sieveMap = new Map([
236
+ length,
237
+ order,
238
+ order2,
239
+ bias,
240
+ unbias,
241
+ crossing,
242
+ attaching,
243
+ similar,
244
+ orientation,
245
+ integrity,
246
+ strong,
247
+ weak,
248
+ ].map((x) => [x.title, x]));
249
+ /**
250
+ * 选择最优的拆分方案
251
+ *
252
+ * @param config - 配置
253
+ * @param component - 待拆分部件
254
+ * @param schemeList - 拆分方案列表
255
+ * @param rootMap - 字根映射,从切片的二进制表示到字根名称的映射
256
+ */
257
+ export const select = (config, component, schemeList, rootMap, requiredRoots) => {
258
+ const environment = {
259
+ component,
260
+ rootMap,
261
+ ...config,
262
+ };
263
+ const sieveNames = config.analysis.selector ?? defaultSelector;
264
+ const schemeData = schemeList.map((scheme) => {
265
+ const evaluation = new Map();
266
+ for (const sieveName of sieveNames) {
267
+ const sieve = sieveMap.get(sieveName);
268
+ const value = sieve.key(scheme, environment);
269
+ evaluation.set(sieve.title, value);
270
+ }
271
+ return {
272
+ scheme,
273
+ roots: scheme.map((x) => rootMap.get(x)),
274
+ evaluation,
275
+ optional: false,
276
+ };
277
+ });
278
+ schemeData.sort((a, b) => {
279
+ for (const sieveName of sieveNames) {
280
+ const aValue = a.evaluation.get(sieveName);
281
+ const bValue = b.evaluation.get(sieveName);
282
+ if (isLess(aValue, bValue))
283
+ return -1;
284
+ if (isLess(bValue, aValue))
285
+ return 1;
286
+ continue;
287
+ }
288
+ return 0;
289
+ });
290
+ const requiredRootsNames = Array.from(requiredRoots).map((x) => rootMap.get(x));
291
+ const optionalRootsData = schemeData.map((x) => x.roots.filter((y) => !requiredRootsNames.includes(y)));
292
+ for (const [index, data] of schemeData.entries()) {
293
+ data.optional = true;
294
+ for (let prevIndex = 0; prevIndex != index; ++prevIndex) {
295
+ const current = optionalRootsData[index];
296
+ const previous = optionalRootsData[prevIndex];
297
+ if (previous.every((x) => current.includes(x))) {
298
+ data.optional = false;
299
+ break;
300
+ }
301
+ }
302
+ if (data.scheme.every((x) => requiredRoots.has(x) || (x & (x - 1)) === 0)) {
303
+ break;
304
+ }
305
+ }
306
+ // If there is no scheme, error
307
+ if (schemeData.length === 0)
308
+ return new NoSchemeError();
309
+ return schemeData;
310
+ };
@@ -0,0 +1,41 @@
1
+ import type { SVGGlyph, SVGStroke } from "./data.js";
2
+ import type { Curve, Position } from "./bezier.js";
3
+ interface RenderedStroke extends Pick<SVGStroke, "feature"> {
4
+ curveList: Curve[];
5
+ }
6
+ type RenderedGlyph = RenderedStroke[];
7
+ interface CrossRelation {
8
+ type: "交";
9
+ }
10
+ interface AttachRelation {
11
+ type: "连";
12
+ first: "前" | "中" | "后";
13
+ second: "前" | "中" | "后";
14
+ }
15
+ interface ParallelRelation {
16
+ type: "平行";
17
+ mainAxis: Position;
18
+ crossAxis: Position;
19
+ }
20
+ interface PerpendicularRelation {
21
+ type: "散";
22
+ x: Position;
23
+ y: Position;
24
+ }
25
+ type DisjointRelation = ParallelRelation | PerpendicularRelation;
26
+ type CurveRelation = CrossRelation | AttachRelation | DisjointRelation;
27
+ declare const curveRelation: (c1: Curve, c2: Curve) => CurveRelation;
28
+ declare const strokeRelation: (stroke1: RenderedStroke, stroke2: RenderedStroke) => StrokeRelation;
29
+ type StrokeRelation = CurveRelation[];
30
+ declare const renderSVGStroke: ({ feature, start, curveList }: SVGStroke) => RenderedStroke;
31
+ declare const renderSVGGlyph: (glyph: SVGGlyph) => RenderedStroke[];
32
+ interface Topology {
33
+ matrix: StrokeRelation[][];
34
+ orientedPairs: [number, number][];
35
+ }
36
+ declare const isConforming: (r1: CurveRelation, r2: CurveRelation) => boolean;
37
+ export type CornerSpecifier = [number, number, number, number];
38
+ declare const findCorners: (renderedGlyph: RenderedGlyph) => CornerSpecifier;
39
+ declare const findTopology: (renderedGlyph: RenderedGlyph) => Topology;
40
+ export { isConforming, curveRelation, strokeRelation, renderSVGStroke, renderSVGGlyph, findTopology, findCorners, };
41
+ export type { CurveRelation, StrokeRelation, RenderedGlyph, Topology };
@@ -0,0 +1,209 @@
1
+ import { isEqual, minBy } from "lodash-es";
2
+ import { subtract, distance } from "./mathjs.js";
3
+ import { area, findCrossPoint, getBoundingBox, getIntervalOnOrientation, getIntervalPosition, isCollinear, makeCurve, } from "./bezier.js";
4
+ const makeAttachRelation = (first, second) => ({ type: "连", first, second });
5
+ const getParallelRelation = (a, b) => {
6
+ const [amain, across] = getIntervalOnOrientation(a);
7
+ const [bmain, bcross] = getIntervalOnOrientation(b);
8
+ const mainPosition = getIntervalPosition(amain, bmain);
9
+ const crossPosition = getIntervalPosition(across, bcross);
10
+ return { type: "平行", mainAxis: mainPosition, crossAxis: crossPosition };
11
+ };
12
+ const getDisjointRelation = (a, b) => {
13
+ if (a.orientation === b.orientation)
14
+ return getParallelRelation(a, b);
15
+ const [astart, aend] = getBoundingBox(a);
16
+ const [bstart, bend] = getBoundingBox(b);
17
+ return {
18
+ type: "散",
19
+ x: getIntervalPosition([astart[0], aend[0]], [bstart[0], bend[0]]),
20
+ y: getIntervalPosition([astart[1], aend[1]], [bstart[1], bend[1]]),
21
+ };
22
+ };
23
+ const getAttachRelation = (a, b) => {
24
+ const [astart, aend] = getBoundingBox(a);
25
+ const [bstart, bend] = getBoundingBox(b);
26
+ if (isEqual(astart, bstart))
27
+ return makeAttachRelation("前", "前");
28
+ if (isEqual(astart, bend))
29
+ return makeAttachRelation("前", "后");
30
+ if (isEqual(aend, bstart))
31
+ return makeAttachRelation("后", "前");
32
+ if (isEqual(aend, bend))
33
+ return makeAttachRelation("后", "后");
34
+ if (a.type === "linear") {
35
+ if (isCollinear(astart, aend, bstart))
36
+ return makeAttachRelation("中", "前");
37
+ if (isCollinear(astart, aend, bend))
38
+ return makeAttachRelation("中", "后");
39
+ }
40
+ if (b.type === "linear") {
41
+ if (isCollinear(bstart, bend, astart))
42
+ return makeAttachRelation("前", "中");
43
+ if (isCollinear(bstart, bend, aend))
44
+ return makeAttachRelation("后", "中");
45
+ }
46
+ };
47
+ const linearRelation = (a, b) => {
48
+ const [astart, aend] = a.controls;
49
+ const [bstart, bend] = b.controls;
50
+ const [v, v1, v2] = [
51
+ subtract(aend, astart),
52
+ subtract(bstart, astart),
53
+ subtract(bend, astart),
54
+ ];
55
+ const vc = area(v, v1) * area(v, v2);
56
+ const [u, u1, u2] = [
57
+ subtract(bend, bstart),
58
+ subtract(astart, bstart),
59
+ subtract(aend, bstart),
60
+ ];
61
+ const uc = area(u, u1) * area(u, u2);
62
+ if (vc < 0 && uc < 0) {
63
+ return { type: "交" };
64
+ }
65
+ return getDisjointRelation(a, b);
66
+ };
67
+ const genericRelation = (a, b) => {
68
+ const crossPoint = findCrossPoint(a, [0, 1], b, [0, 1]);
69
+ if (crossPoint === undefined)
70
+ return getDisjointRelation(a, b);
71
+ const [astart, aend] = getBoundingBox(a);
72
+ const [bstart, bend] = getBoundingBox(b);
73
+ const distanceThreshold = 3;
74
+ if (distance(crossPoint, astart) < distanceThreshold)
75
+ return makeAttachRelation("前", "中");
76
+ if (distance(crossPoint, aend) < distanceThreshold)
77
+ return makeAttachRelation("后", "中");
78
+ if (distance(crossPoint, bstart) < distanceThreshold)
79
+ return makeAttachRelation("中", "前");
80
+ if (distance(crossPoint, bend) < distanceThreshold)
81
+ return makeAttachRelation("中", "后");
82
+ return { type: "交" };
83
+ };
84
+ const curveRelation = (c1, c2) => {
85
+ const relation = getAttachRelation(c1, c2);
86
+ if (relation !== undefined)
87
+ return relation;
88
+ if (c1.type === "linear" && c2.type === "linear") {
89
+ return linearRelation(c1, c2);
90
+ }
91
+ return genericRelation(c1, c2);
92
+ };
93
+ const strokeRelation = (stroke1, stroke2) => {
94
+ const strokeRelation = [];
95
+ for (const curve1 of stroke1.curveList) {
96
+ for (const curve2 of stroke2.curveList) {
97
+ strokeRelation.push(curveRelation(curve1, curve2));
98
+ }
99
+ }
100
+ return strokeRelation;
101
+ };
102
+ const renderSVGStroke = ({ feature, start, curveList }) => {
103
+ const r = { feature, curveList: [] };
104
+ let previousPosition = start;
105
+ for (const draw of curveList) {
106
+ const curve = makeCurve(previousPosition, draw);
107
+ previousPosition =
108
+ curve.type === "linear"
109
+ ? curve.controls[1]
110
+ : curve.type === "arc"
111
+ ? curve.controls[1]
112
+ : curve.controls[3];
113
+ r.curveList.push(curve);
114
+ }
115
+ return r;
116
+ };
117
+ const renderSVGGlyph = (glyph) => glyph.map(renderSVGStroke);
118
+ const isConforming = (r1, r2) => {
119
+ switch (r1.type) {
120
+ case "平行":
121
+ return r2.type === "平行";
122
+ case "散":
123
+ return true;
124
+ case "交":
125
+ return (r2.type === "交" ||
126
+ (r2.type === "连" && r2.second === "中") ||
127
+ r2.type === "散");
128
+ case "连":
129
+ if (r1.second === "中") {
130
+ return (r2.type === "交" ||
131
+ r2.type === "散" ||
132
+ (r2.type === "连" && r2.second === "中"));
133
+ }
134
+ return (r2.type === "连" && r2.second === r1.second) || r2.type === "散";
135
+ }
136
+ };
137
+ const hashPoint = (point) => `${point[0]}-${point[1]}`;
138
+ const findCorners = (renderedGlyph) => {
139
+ const extremes = [];
140
+ const pointToStroke = new Map();
141
+ for (const [strokeIndex, { curveList }] of renderedGlyph.entries()) {
142
+ const subExtremes = [];
143
+ for (const [curveIndex, curve] of curveList.entries()) {
144
+ const [start, end] = getBoundingBox(curve);
145
+ if (curveIndex === 0)
146
+ subExtremes.push(start);
147
+ subExtremes.push(end);
148
+ }
149
+ for (const point of subExtremes) {
150
+ const hash = hashPoint(point);
151
+ if (!pointToStroke.has(hash))
152
+ pointToStroke.set(hash, strokeIndex);
153
+ }
154
+ extremes.push(...subExtremes);
155
+ }
156
+ const topLeft = minBy(extremes, (p) => distance(p, [0, 0]));
157
+ const topRight = minBy(extremes, (p) => distance(p, [100, 0]));
158
+ const bottomLeft = minBy(extremes, (p) => distance(p, [0, 100]));
159
+ const bottomRight = minBy(extremes, (p) => distance(p, [100, 100]));
160
+ return [topLeft, topRight, bottomLeft, bottomRight].map((point) => pointToStroke.get(hashPoint(point)));
161
+ };
162
+ const findTopology = (renderedGlyph) => {
163
+ const topology = { matrix: [], orientedPairs: [] };
164
+ for (const [index1, stroke1] of renderedGlyph.entries()) {
165
+ const row = [];
166
+ for (const [index2, stroke2] of renderedGlyph.entries()) {
167
+ if (index1 === index2)
168
+ row.push([]);
169
+ else
170
+ row.push(strokeRelation(stroke1, stroke2));
171
+ }
172
+ topology.matrix.push(row);
173
+ }
174
+ for (const [index1] of renderedGlyph.entries()) {
175
+ for (const [index2] of renderedGlyph.entries()) {
176
+ if (index2 >= index1)
177
+ break;
178
+ const relations = topology.matrix[index1]?.[index2];
179
+ if (relations.some((v) => v.type === "交" || v.type === "连"))
180
+ continue;
181
+ const parallelIndex = relations.findIndex((v) => v.type === "平行" && v.mainAxis === 0);
182
+ if (parallelIndex !== -1) {
183
+ topology.orientedPairs.push([index1, index2]);
184
+ }
185
+ // 想要严谨判断同向笔画特别困难,先不搞了
186
+ // if (parallelIndex === -1) continue;
187
+ // // drop this shit and compute from ab initio
188
+ // const index2Curves = renderedGlyph[index2]!.curveList;
189
+ // // we have: parallelIndex = index1Index * index2Curves + index2Index
190
+ // const index2Index = parallelIndex % index2Curves.length;
191
+ // const index1Index = (parallelIndex - index2Index) / index2Curves.length;
192
+ // // get all curves from all strokes other than k and l
193
+ // const otherStrokes = [...renderedGlyph.keys()].filter(
194
+ // (_, index) => index !== index1 && index !== index2
195
+ // );
196
+ // if (
197
+ // otherStrokes.every((x) => {
198
+ // const r1 = topology.matrix[x]![index1]![index1Index]!;
199
+ // const r2 = topology.matrix[x]![index2]![index2Index]!;
200
+ // return isConforming(r1, r2) && isConforming(r2, r1);
201
+ // })
202
+ // ) {
203
+ // topology.orientedPairs.push([index1, index2]);
204
+ // }
205
+ }
206
+ }
207
+ return topology;
208
+ };
209
+ export { isConforming, curveRelation, strokeRelation, renderSVGStroke, renderSVGGlyph, findTopology, findCorners, };
@@ -0,0 +1,18 @@
1
+ import type { Operator, Repertoire } from "./data.js";
2
+ export interface 变换器 {
3
+ from: 模式;
4
+ to: 模式;
5
+ }
6
+ export type 节点类型 = 模式 | 结构变量 | string;
7
+ export interface 模式 {
8
+ operator: Operator;
9
+ operandList: (string | 模式 | 结构变量)[];
10
+ }
11
+ export interface 结构变量 {
12
+ id: number;
13
+ }
14
+ export declare function isVariable(x: any): x is 结构变量;
15
+ /**
16
+ * 应用变换器到数据库,返回新的数据库。
17
+ */
18
+ export declare function 应用变换器(字库: Repertoire, 变换器: 变换器): Repertoire;
@@ -0,0 +1,111 @@
1
+ export function isVariable(x) {
2
+ return x && typeof x.id === "number";
3
+ }
4
+ /**
5
+ * 把 replacement pattern / variable / string 扁平化并写入 dbOut,返回引用该子树的 key。
6
+ * - 模式中的字符串直接视为已有的键引用
7
+ * - 变量使用 varMap 中绑定的键
8
+ * - 嵌套 pattern 递归生成子键
9
+ */
10
+ function 替换(项, 映射, 输出, 签名缓存, 生成计数, 目标字符) {
11
+ if (typeof 项 === "string")
12
+ return 项;
13
+ if (isVariable(项)) {
14
+ const k = 映射[项.id];
15
+ if (!k)
16
+ throw new Error(`unbound variable $${项.id}`);
17
+ return k;
18
+ }
19
+ // Pattern
20
+ const 子键们 = 项.operandList.map((op) => 替换(op, 映射, 输出, 签名缓存, 生成计数));
21
+ const sig = `${项.operator}:${子键们.join(",")}`;
22
+ if (目标字符 === undefined) {
23
+ const existing = 签名缓存.get(sig);
24
+ if (existing)
25
+ return existing;
26
+ }
27
+ const 字符 = 目标字符 ?? String.fromCodePoint(生成计数.c++);
28
+ const 复合体 = {
29
+ type: "compound",
30
+ operator: 项.operator,
31
+ operandList: 子键们,
32
+ };
33
+ 输出[字符] = {
34
+ unicode: 字符.codePointAt(0),
35
+ gb2312: 0,
36
+ tygf: 0,
37
+ gf0014_id: null,
38
+ gf3001_id: null,
39
+ name: null,
40
+ glyph: 复合体,
41
+ readings: [],
42
+ };
43
+ 签名缓存.set(sig, 字符);
44
+ return 字符;
45
+ }
46
+ /**
47
+ * 在数据库上匹配模式到某个键(按需展开并递归调用自身),
48
+ * 变量绑定为子键字符串。
49
+ */
50
+ function 匹配模式到键(模式, 字符, 变量映射, 字库) {
51
+ const 复合体 = 字库[字符]?.glyph;
52
+ if (!复合体 || 复合体.type !== "compound")
53
+ return false;
54
+ if (模式.operator !== 复合体.operator)
55
+ return false;
56
+ if (模式.operandList.length !== 复合体.operandList.length)
57
+ return false;
58
+ for (let i = 0; i < 模式.operandList.length; i++) {
59
+ const 操作数 = 模式.operandList[i];
60
+ const 子键 = 复合体.operandList[i];
61
+ if (typeof 操作数 === "string") {
62
+ if (子键 !== 操作数)
63
+ return false;
64
+ }
65
+ else if (isVariable(操作数)) {
66
+ const id = 操作数.id;
67
+ const 已绑定 = 变量映射[id];
68
+ if (已绑定 === undefined) {
69
+ 变量映射[id] = 子键;
70
+ }
71
+ else {
72
+ if (已绑定 !== 子键)
73
+ return false;
74
+ }
75
+ }
76
+ else {
77
+ // 嵌套 pattern,递归匹配对应的子键
78
+ if (!匹配模式到键(操作数, 子键, 变量映射, 字库))
79
+ return false;
80
+ }
81
+ }
82
+ return true;
83
+ }
84
+ /**
85
+ * 应用变换器到数据库,返回新的数据库。
86
+ */
87
+ export function 应用变换器(字库, 变换器) {
88
+ // 逐键匹配(按需展开)
89
+ const 匹配集合 = new Map();
90
+ const 签名缓存 = new Map();
91
+ let 新字符起始码位 = 0x100000;
92
+ for (const 字符 of Object.keys(字库)) {
93
+ const 码位 = 字符.codePointAt(0);
94
+ if (码位 >= 0x100000) {
95
+ 新字符起始码位 = Math.max(新字符起始码位, 码位 + 1);
96
+ }
97
+ const c = 字库[字符];
98
+ const sig = JSON.stringify(c.glyph);
99
+ 签名缓存.set(sig, 字符);
100
+ const 变量映射 = {};
101
+ if (匹配模式到键(变换器.from, 字符, 变量映射, 字库)) {
102
+ 匹配集合.set(字符, 变量映射);
103
+ }
104
+ }
105
+ const 输出数据库 = { ...字库 };
106
+ const 生成计数 = { c: 新字符起始码位 };
107
+ for (const [键, 变量映射] of 匹配集合) {
108
+ 替换(变换器.to, 变量映射, 输出数据库, 签名缓存, 生成计数, 键);
109
+ }
110
+ return 输出数据库;
111
+ }