@retikz/core 0.1.0-alpha.2 → 0.1.0-alpha.4
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/dist/es/compile/compile.d.ts +6 -0
- package/dist/es/compile/compile.d.ts.map +1 -1
- package/dist/es/compile/compile.js +33 -4
- package/dist/es/compile/node.d.ts +23 -2
- package/dist/es/compile/node.d.ts.map +1 -1
- package/dist/es/compile/node.js +94 -4
- package/dist/es/compile/path.d.ts +3 -2
- package/dist/es/compile/path.d.ts.map +1 -1
- package/dist/es/compile/path.js +333 -14
- package/dist/es/compile/position.d.ts +8 -5
- package/dist/es/compile/position.d.ts.map +1 -1
- package/dist/es/compile/position.js +32 -5
- package/dist/es/geometry/arc.d.ts +34 -0
- package/dist/es/geometry/arc.d.ts.map +1 -0
- package/dist/es/geometry/arc.js +53 -0
- package/dist/es/geometry/bend.d.ts +18 -0
- package/dist/es/geometry/bend.d.ts.map +1 -0
- package/dist/es/geometry/bend.js +29 -0
- package/dist/es/geometry/index.d.ts +3 -0
- package/dist/es/geometry/index.d.ts.map +1 -1
- package/dist/es/geometry/segment.d.ts +38 -0
- package/dist/es/geometry/segment.d.ts.map +1 -0
- package/dist/es/geometry/segment.js +82 -0
- package/dist/es/index.d.ts +4 -4
- package/dist/es/index.d.ts.map +1 -1
- package/dist/es/index.js +7 -4
- package/dist/es/ir/coordinate.d.ts +57 -0
- package/dist/es/ir/coordinate.d.ts.map +1 -0
- package/dist/es/ir/coordinate.js +27 -0
- package/dist/es/ir/index.d.ts +1 -0
- package/dist/es/ir/index.d.ts.map +1 -1
- package/dist/es/ir/node.d.ts +276 -17
- package/dist/es/ir/node.d.ts.map +1 -1
- package/dist/es/ir/node.js +28 -3
- package/dist/es/ir/path/path.d.ts +625 -15
- package/dist/es/ir/path/path.d.ts.map +1 -1
- package/dist/es/ir/path/path.js +22 -0
- package/dist/es/ir/path/step.d.ts +872 -20
- package/dist/es/ir/path/step.d.ts.map +1 -1
- package/dist/es/ir/path/step.js +86 -4
- package/dist/es/ir/path/target.d.ts +32 -2
- package/dist/es/ir/path/target.d.ts.map +1 -1
- package/dist/es/ir/path/target.js +7 -3
- package/dist/es/ir/position/at-position.d.ts +50 -0
- package/dist/es/ir/position/at-position.d.ts.map +1 -0
- package/dist/es/ir/position/at-position.js +30 -0
- package/dist/es/ir/position/index.d.ts +1 -0
- package/dist/es/ir/position/index.d.ts.map +1 -1
- package/dist/es/ir/scene.d.ts +2072 -112
- package/dist/es/ir/scene.d.ts.map +1 -1
- package/dist/es/ir/scene.js +6 -1
- package/dist/es/parsers/index.d.ts +1 -0
- package/dist/es/parsers/index.d.ts.map +1 -1
- package/dist/es/parsers/parseTargetSugar.d.ts +3 -0
- package/dist/es/parsers/parseTargetSugar.d.ts.map +1 -0
- package/dist/es/parsers/parseTargetSugar.js +31 -0
- package/dist/es/parsers/parseWay.d.ts +131 -22
- package/dist/es/parsers/parseWay.d.ts.map +1 -1
- package/dist/es/parsers/parseWay.js +149 -17
- package/dist/lib/compile/compile.cjs +33 -4
- package/dist/lib/compile/compile.d.ts +6 -0
- package/dist/lib/compile/compile.d.ts.map +1 -1
- package/dist/lib/compile/node.cjs +94 -4
- package/dist/lib/compile/node.d.ts +23 -2
- package/dist/lib/compile/node.d.ts.map +1 -1
- package/dist/lib/compile/path.cjs +333 -14
- package/dist/lib/compile/path.d.ts +3 -2
- package/dist/lib/compile/path.d.ts.map +1 -1
- package/dist/lib/compile/position.cjs +32 -5
- package/dist/lib/compile/position.d.ts +8 -5
- package/dist/lib/compile/position.d.ts.map +1 -1
- package/dist/lib/geometry/arc.cjs +55 -0
- package/dist/lib/geometry/arc.d.ts +34 -0
- package/dist/lib/geometry/arc.d.ts.map +1 -0
- package/dist/lib/geometry/bend.cjs +29 -0
- package/dist/lib/geometry/bend.d.ts +18 -0
- package/dist/lib/geometry/bend.d.ts.map +1 -0
- package/dist/lib/geometry/index.d.ts +3 -0
- package/dist/lib/geometry/index.d.ts.map +1 -1
- package/dist/lib/geometry/segment.cjs +88 -0
- package/dist/lib/geometry/segment.d.ts +38 -0
- package/dist/lib/geometry/segment.d.ts.map +1 -0
- package/dist/lib/index.cjs +18 -0
- package/dist/lib/index.d.ts +4 -4
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/ir/coordinate.cjs +27 -0
- package/dist/lib/ir/coordinate.d.ts +57 -0
- package/dist/lib/ir/coordinate.d.ts.map +1 -0
- package/dist/lib/ir/index.d.ts +1 -0
- package/dist/lib/ir/index.d.ts.map +1 -1
- package/dist/lib/ir/node.cjs +28 -2
- package/dist/lib/ir/node.d.ts +276 -17
- package/dist/lib/ir/node.d.ts.map +1 -1
- package/dist/lib/ir/path/path.cjs +22 -0
- package/dist/lib/ir/path/path.d.ts +625 -15
- package/dist/lib/ir/path/path.d.ts.map +1 -1
- package/dist/lib/ir/path/step.cjs +93 -3
- package/dist/lib/ir/path/step.d.ts +872 -20
- package/dist/lib/ir/path/step.d.ts.map +1 -1
- package/dist/lib/ir/path/target.cjs +8 -2
- package/dist/lib/ir/path/target.d.ts +32 -2
- package/dist/lib/ir/path/target.d.ts.map +1 -1
- package/dist/lib/ir/position/at-position.cjs +31 -0
- package/dist/lib/ir/position/at-position.d.ts +50 -0
- package/dist/lib/ir/position/at-position.d.ts.map +1 -0
- package/dist/lib/ir/position/index.d.ts +1 -0
- package/dist/lib/ir/position/index.d.ts.map +1 -1
- package/dist/lib/ir/scene.cjs +6 -1
- package/dist/lib/ir/scene.d.ts +2072 -112
- package/dist/lib/ir/scene.d.ts.map +1 -1
- package/dist/lib/parsers/index.d.ts +1 -0
- package/dist/lib/parsers/index.d.ts.map +1 -1
- package/dist/lib/parsers/parseTargetSugar.cjs +31 -0
- package/dist/lib/parsers/parseTargetSugar.d.ts +3 -0
- package/dist/lib/parsers/parseTargetSugar.d.ts.map +1 -0
- package/dist/lib/parsers/parseWay.cjs +149 -17
- package/dist/lib/parsers/parseWay.d.ts +131 -22
- package/dist/lib/parsers/parseWay.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/es/compile/path.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { resolvePosition } from "./position.js";
|
|
2
2
|
import { anchorOf, angleBoundaryOf, boundaryPointOf } from "./node.js";
|
|
3
|
+
import { arcBoundingPoints, arcEndPoint, arcSvgFlags } from "../geometry/arc.js";
|
|
4
|
+
import { bendControlPoints } from "../geometry/bend.js";
|
|
5
|
+
import { arcSegmentSample, circleSegmentSample, cubicSegmentSample, ellipseSegmentSample, foldSegmentSample, lineSegmentSample, quadSegmentSample } from "../geometry/segment.js";
|
|
3
6
|
import { parseNodeRef } from "./parseTarget.js";
|
|
7
|
+
import { fallbackMeasurer } from "./text-metrics.js";
|
|
4
8
|
//#region src/compile/path.ts
|
|
5
9
|
/**
|
|
6
10
|
* IR 的 path-level `arrow` + `arrowShape` 映射到 PathPrim 的
|
|
@@ -72,6 +76,7 @@ var refPointOfTarget = (target, nodeIndex) => {
|
|
|
72
76
|
case "angle": return angleBoundaryOf(node, ref.angle);
|
|
73
77
|
}
|
|
74
78
|
}
|
|
79
|
+
if (typeof target === "object" && !Array.isArray(target) && ("rel" in target || "relAccumulate" in target)) return null;
|
|
75
80
|
return resolvePosition(target, nodeIndex);
|
|
76
81
|
};
|
|
77
82
|
/**
|
|
@@ -99,11 +104,179 @@ var clipForTarget = (target, toward, nodeIndex) => {
|
|
|
99
104
|
case "angle": return angleBoundaryOf(node, ref.angle);
|
|
100
105
|
}
|
|
101
106
|
}
|
|
107
|
+
if (typeof target === "object" && !Array.isArray(target) && ("rel" in target || "relAccumulate" in target)) return null;
|
|
102
108
|
return resolvePosition(target, nodeIndex);
|
|
103
109
|
};
|
|
104
110
|
/** 浅相等:两个 IRPosition 的两个分量都精确相等(未 round) */
|
|
105
111
|
var samePoint = (a, b) => !!a && !!b && a[0] === b[0] && a[1] === b[1];
|
|
106
112
|
/**
|
|
113
|
+
* 语义 stroke 档位 → 数值映射(user units)。
|
|
114
|
+
*
|
|
115
|
+
* 对齐 TikZ 比例(thin = 默认 0.4pt,retikz 默认 strokeWidth = 1,所以 thin → 1):
|
|
116
|
+
* ultra thin 0.1pt → 0.25
|
|
117
|
+
* very thin 0.2pt → 0.5
|
|
118
|
+
* thin 0.4pt → 1 (= 默认 strokeWidth)
|
|
119
|
+
* semithick 0.6pt → 1.5
|
|
120
|
+
* thick 0.8pt → 2
|
|
121
|
+
* very thick 1.2pt → 3
|
|
122
|
+
* ultra thick 1.6pt → 4
|
|
123
|
+
*
|
|
124
|
+
* 显式 `strokeWidth` 始终覆盖 `thickness`——thickness 仅在 strokeWidth 缺省时生效。
|
|
125
|
+
*/
|
|
126
|
+
var THICKNESS_TO_WIDTH = {
|
|
127
|
+
ultraThin: .25,
|
|
128
|
+
veryThin: .5,
|
|
129
|
+
thin: 1,
|
|
130
|
+
semithick: 1.5,
|
|
131
|
+
thick: 2,
|
|
132
|
+
veryThick: 3,
|
|
133
|
+
ultraThick: 4
|
|
134
|
+
};
|
|
135
|
+
/** ADR-0004:边标注的默认字号 / 偏移量(user units) */
|
|
136
|
+
var LABEL_FONT_SIZE = 14;
|
|
137
|
+
var LABEL_LINE_HEIGHT_FACTOR = 1.2;
|
|
138
|
+
var LABEL_SIDE_OFFSET = 4;
|
|
139
|
+
var RAD_TO_DEG = 180 / Math.PI;
|
|
140
|
+
/** label.position → 段参数 t */
|
|
141
|
+
var tForLabelPosition = (pos) => pos === "near-start" ? .25 : pos === "near-end" ? .75 : .5;
|
|
142
|
+
/**
|
|
143
|
+
* 把 step.label + 段采样结果翻成 TextPrim(sloped 时再裹一层 group 旋转)。
|
|
144
|
+
*
|
|
145
|
+
* 几何(默认 side='above',position='midway'):
|
|
146
|
+
* - 'above': 锚点 (x, y - LABEL_SIDE_OFFSET),align=middle, baseline=bottom
|
|
147
|
+
* - 'below': 锚点 (x, y + LABEL_SIDE_OFFSET),align=middle, baseline=top
|
|
148
|
+
* - 'left' : 锚点 (x - LABEL_SIDE_OFFSET, y),align=end, baseline=middle
|
|
149
|
+
* - 'right': 锚点 (x + LABEL_SIDE_OFFSET, y),align=start, baseline=middle
|
|
150
|
+
* - 'sloped': 锚点 (x, y) 不偏移,align=middle baseline=bottom,外裹 group
|
|
151
|
+
* `transform="rotate(angle x y)"`,angle 由切线 atan2 算出(SVG y-down,CW 正向)
|
|
152
|
+
*
|
|
153
|
+
* 文本宽高交给 measureText;fallbackMeasurer 时只是估算,但不影响渲染坐标。
|
|
154
|
+
* 返回 primitive + 用于 viewBox 的若干外接点(sloped 时按"近似最大半径"四角扩张)。
|
|
155
|
+
*/
|
|
156
|
+
var emitLabelPrimitive = (label, sample, measureText, round) => {
|
|
157
|
+
const fontSize = LABEL_FONT_SIZE;
|
|
158
|
+
const lineHeight = fontSize * LABEL_LINE_HEIGHT_FACTOR;
|
|
159
|
+
const m = measureText(label.text, { size: fontSize });
|
|
160
|
+
const measuredWidth = m.width;
|
|
161
|
+
const measuredHeight = m.height || lineHeight;
|
|
162
|
+
const side = label.side ?? "above";
|
|
163
|
+
let x = sample.point[0];
|
|
164
|
+
let y = sample.point[1];
|
|
165
|
+
let align = "middle";
|
|
166
|
+
let baseline = "middle";
|
|
167
|
+
if (side === "above") {
|
|
168
|
+
y -= LABEL_SIDE_OFFSET;
|
|
169
|
+
baseline = "bottom";
|
|
170
|
+
} else if (side === "below") {
|
|
171
|
+
y += LABEL_SIDE_OFFSET;
|
|
172
|
+
baseline = "top";
|
|
173
|
+
} else if (side === "left") {
|
|
174
|
+
x -= LABEL_SIDE_OFFSET;
|
|
175
|
+
align = "end";
|
|
176
|
+
} else if (side === "right") {
|
|
177
|
+
x += LABEL_SIDE_OFFSET;
|
|
178
|
+
align = "start";
|
|
179
|
+
} else baseline = "bottom";
|
|
180
|
+
const text = {
|
|
181
|
+
type: "text",
|
|
182
|
+
x: round(x),
|
|
183
|
+
y: round(y),
|
|
184
|
+
lines: [{ text: label.text }],
|
|
185
|
+
fontSize,
|
|
186
|
+
align,
|
|
187
|
+
baseline,
|
|
188
|
+
lineHeight: round(lineHeight),
|
|
189
|
+
measuredWidth: round(measuredWidth),
|
|
190
|
+
measuredHeight: round(measuredHeight),
|
|
191
|
+
fill: "currentColor"
|
|
192
|
+
};
|
|
193
|
+
if (side === "sloped") {
|
|
194
|
+
const groupPrim = {
|
|
195
|
+
type: "group",
|
|
196
|
+
transform: `rotate(${round(Math.atan2(sample.tangent[1], sample.tangent[0]) * RAD_TO_DEG)} ${round(x)} ${round(y)})`,
|
|
197
|
+
children: [text]
|
|
198
|
+
};
|
|
199
|
+
const r = Math.max(measuredWidth / 2, measuredHeight / 2);
|
|
200
|
+
return {
|
|
201
|
+
primitive: groupPrim,
|
|
202
|
+
points: [
|
|
203
|
+
[x - r, y - r],
|
|
204
|
+
[x + r, y - r],
|
|
205
|
+
[x - r, y + r],
|
|
206
|
+
[x + r, y + r]
|
|
207
|
+
]
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const halfW = measuredWidth / 2;
|
|
211
|
+
const halfH = measuredHeight / 2;
|
|
212
|
+
return {
|
|
213
|
+
primitive: text,
|
|
214
|
+
points: [
|
|
215
|
+
[x - halfW, y - halfH],
|
|
216
|
+
[x + halfW, y - halfH],
|
|
217
|
+
[x - halfW, y + halfH],
|
|
218
|
+
[x + halfW, y + halfH]
|
|
219
|
+
]
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* 把 rel / relAccumulate 目标解析为绝对 Position,产出 step kind 不变但
|
|
224
|
+
* `to` 字段全为绝对坐标的步序列。
|
|
225
|
+
*
|
|
226
|
+
* 决策(ADR-0003 §影响 与 §背景 文本有矛盾):
|
|
227
|
+
* 两者都相对 prevEnd 解析,区别仅在是否更新 prevEnd——
|
|
228
|
+
* rel 不更新(保持 TikZ `+` 语义),relAccumulate 更新(TikZ `++` 累积)。
|
|
229
|
+
* 选这个语义因为:(1) 与 TikZ `+`/`++` 一致;(2) 与字段名"Accumulate"
|
|
230
|
+
* 语义匹配;(3) 与 ADR 背景段一致;§影响 段写"pathStart + offset"是 typo。
|
|
231
|
+
*
|
|
232
|
+
* 跨 step kind 的 prevEnd 推进:
|
|
233
|
+
* - 有 to 的 kind(move/line/step/curve/cubic/bend):prevEnd = refPointOfTarget(to)
|
|
234
|
+
* - arc:prevEnd = arcEndPoint(prevEnd, radius, endAngle)
|
|
235
|
+
* - circlePath / ellipsePath:prevEnd 不变(画完留圆心,即 prevEnd 本身)
|
|
236
|
+
* - cycle:prevEnd 不变(不重置到 pathStart,保持简单;后续如有需要再扩)
|
|
237
|
+
*
|
|
238
|
+
* prevEnd 为 null(首步是 rel)时回退到 [0, 0] 当锚点;解析失败保持原 step。
|
|
239
|
+
*/
|
|
240
|
+
var normalizeRelativeTargets = (steps, nodeIndex) => {
|
|
241
|
+
let prevEnd = null;
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const step of steps) {
|
|
244
|
+
if (step.kind === "cycle") {
|
|
245
|
+
out.push(step);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (step.kind === "circlePath" || step.kind === "ellipsePath") {
|
|
249
|
+
out.push(step);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (step.kind === "arc") {
|
|
253
|
+
out.push(step);
|
|
254
|
+
if (prevEnd) prevEnd = arcEndPoint(prevEnd, step.radius, step.endAngle);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const original = step.to;
|
|
258
|
+
let resolvedTo = original;
|
|
259
|
+
let updatePrevEnd = true;
|
|
260
|
+
if (typeof original === "object" && !Array.isArray(original) && "rel" in original) {
|
|
261
|
+
const ref = prevEnd ?? [0, 0];
|
|
262
|
+
resolvedTo = [ref[0] + original.rel[0], ref[1] + original.rel[1]];
|
|
263
|
+
updatePrevEnd = false;
|
|
264
|
+
} else if (typeof original === "object" && !Array.isArray(original) && "relAccumulate" in original) {
|
|
265
|
+
const ref = prevEnd ?? [0, 0];
|
|
266
|
+
resolvedTo = [ref[0] + original.relAccumulate[0], ref[1] + original.relAccumulate[1]];
|
|
267
|
+
}
|
|
268
|
+
out.push({
|
|
269
|
+
...step,
|
|
270
|
+
to: resolvedTo
|
|
271
|
+
});
|
|
272
|
+
if (updatePrevEnd) {
|
|
273
|
+
const pos = refPointOfTarget(resolvedTo, nodeIndex);
|
|
274
|
+
if (pos) prevEnd = pos;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
107
280
|
* 把 IR Path 翻译为单个 PathPrim。
|
|
108
281
|
*
|
|
109
282
|
* 关键算法(v0.1.0-alpha.1):每个绘制段(line / fold)**独立**地用节点中心
|
|
@@ -124,15 +297,27 @@ var samePoint = (a, b) => !!a && !!b && a[0] === b[0] && a[1] === b[1];
|
|
|
124
297
|
*
|
|
125
298
|
* 引用未定义节点 / 解析失败时返回 null(path 整体跳过)。
|
|
126
299
|
*/
|
|
127
|
-
var emitPathPrimitive = (path, nodeIndex, round) => {
|
|
128
|
-
const steps = path.children;
|
|
300
|
+
var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer) => {
|
|
301
|
+
const steps = normalizeRelativeTargets(path.children, nodeIndex);
|
|
129
302
|
if (steps.length < 2) return null;
|
|
130
|
-
|
|
131
|
-
|
|
303
|
+
/** ADR-0004:每段 step.label 翻译出的 TextPrim(或裹 sloped 旋转的 group),
|
|
304
|
+
* 与 path 主体 primitive 同级返回;调用方 push 进 scene.primitives */
|
|
305
|
+
const labelPrims = [];
|
|
306
|
+
/** 为当前 step 收 label:算 sample 后调用 emitLabelPrimitive,结果累积到 labelPrims / points */
|
|
307
|
+
const collectLabel = (step, sampleAt) => {
|
|
308
|
+
if (step.kind === "move" || step.kind === "cycle" || !("label" in step) || !step.label) return;
|
|
309
|
+
const sample = sampleAt(tForLabelPosition(step.label.position));
|
|
310
|
+
const r = emitLabelPrimitive(step.label, sample, measureText, round);
|
|
311
|
+
labelPrims.push(r.primitive);
|
|
312
|
+
for (const p of r.points) points.push(p);
|
|
313
|
+
};
|
|
314
|
+
const hasTo = (s) => s.kind !== "cycle" && s.kind !== "arc" && s.kind !== "circlePath" && s.kind !== "ellipsePath";
|
|
315
|
+
const anchors = steps.map((s) => hasTo(s) ? refPointOfTarget(s.to, nodeIndex) : null);
|
|
316
|
+
/** 找 i 之前最近的"有 to 字段的 step" + 它的 anchor */
|
|
132
317
|
const findPrev = (i) => {
|
|
133
318
|
for (let j = i - 1; j >= 0; j--) {
|
|
134
319
|
const s = steps[j];
|
|
135
|
-
if (s
|
|
320
|
+
if (!hasTo(s)) continue;
|
|
136
321
|
const a = anchors[j];
|
|
137
322
|
if (!a) return null;
|
|
138
323
|
return {
|
|
@@ -144,13 +329,27 @@ var emitPathPrimitive = (path, nodeIndex, round) => {
|
|
|
144
329
|
};
|
|
145
330
|
/** 找 i 之前最近的 move 的 to——cycle 闭合的目标 */
|
|
146
331
|
const findRecentMoveTo = (i) => {
|
|
147
|
-
for (let j = i - 1; j >= 0; j--)
|
|
332
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
333
|
+
const s = steps[j];
|
|
334
|
+
if (s.kind === "move") return s.to;
|
|
335
|
+
}
|
|
148
336
|
return null;
|
|
149
337
|
};
|
|
150
338
|
const ops = [];
|
|
151
339
|
const points = [];
|
|
152
340
|
let lastEnd = null;
|
|
153
341
|
let subPathStart = null;
|
|
342
|
+
/**
|
|
343
|
+
* "笔位覆盖"——arc / circlePath / ellipsePath 这种没有 `to` 字段的 step
|
|
344
|
+
* 不能通过 `prev.step.to` 重算下一段的起点。它们设置 penOverride,下一个
|
|
345
|
+
* 绘制段(line/curve/cubic/bend/step)直接用这个点当 fromClip,之后清空。
|
|
346
|
+
*
|
|
347
|
+
* - arc:endpoint(弧终点)—— 与 SVG 实际 cursor 一致
|
|
348
|
+
* - circlePath/ellipsePath:center(圆心)—— ADR-0002 决策"画完留在圆心",
|
|
349
|
+
* 注意 SVG 实际 cursor 在弧端点而不在 center,必须靠 startSegment 发 M
|
|
350
|
+
* teleport 回中心
|
|
351
|
+
*/
|
|
352
|
+
let penOverride = null;
|
|
154
353
|
const emitM = (p) => {
|
|
155
354
|
ops.push({
|
|
156
355
|
cmd: "M",
|
|
@@ -172,6 +371,40 @@ var emitPathPrimitive = (path, nodeIndex, round) => {
|
|
|
172
371
|
ops.push({ cmd: "Z" });
|
|
173
372
|
lastEnd = subPathStart;
|
|
174
373
|
};
|
|
374
|
+
const emitQ = (control, p) => {
|
|
375
|
+
ops.push({
|
|
376
|
+
cmd: "Q",
|
|
377
|
+
control,
|
|
378
|
+
point: p
|
|
379
|
+
});
|
|
380
|
+
points.push(control);
|
|
381
|
+
points.push(p);
|
|
382
|
+
lastEnd = p;
|
|
383
|
+
};
|
|
384
|
+
const emitC = (c1, c2, p) => {
|
|
385
|
+
ops.push({
|
|
386
|
+
cmd: "C",
|
|
387
|
+
control1: c1,
|
|
388
|
+
control2: c2,
|
|
389
|
+
point: p
|
|
390
|
+
});
|
|
391
|
+
points.push(c1);
|
|
392
|
+
points.push(c2);
|
|
393
|
+
points.push(p);
|
|
394
|
+
lastEnd = p;
|
|
395
|
+
};
|
|
396
|
+
const emitA = (rx, ry, largeArc, sweep, p) => {
|
|
397
|
+
ops.push({
|
|
398
|
+
cmd: "A",
|
|
399
|
+
rx,
|
|
400
|
+
ry,
|
|
401
|
+
largeArc,
|
|
402
|
+
sweep,
|
|
403
|
+
point: p
|
|
404
|
+
});
|
|
405
|
+
points.push(p);
|
|
406
|
+
lastEnd = p;
|
|
407
|
+
};
|
|
175
408
|
/** 段起点:与 lastEnd 相同就复用 cursor(省掉冗余 M),否则发 M */
|
|
176
409
|
const startSegment = (p) => {
|
|
177
410
|
if (samePoint(p, lastEnd)) return;
|
|
@@ -199,31 +432,114 @@ var emitPathPrimitive = (path, nodeIndex, round) => {
|
|
|
199
432
|
}
|
|
200
433
|
const prev = findPrev(i);
|
|
201
434
|
if (!prev) return null;
|
|
435
|
+
if (step.kind === "arc") {
|
|
436
|
+
const center = prev.anchor;
|
|
437
|
+
const startPt = arcEndPoint(center, step.radius, step.startAngle);
|
|
438
|
+
const endPt = arcEndPoint(center, step.radius, step.endAngle);
|
|
439
|
+
const flags = arcSvgFlags(step.startAngle, step.endAngle);
|
|
440
|
+
startSegment(startPt);
|
|
441
|
+
emitA(step.radius, step.radius, flags.largeArc, flags.sweep, endPt);
|
|
442
|
+
for (const p of arcBoundingPoints(center, step.radius, step.startAngle, step.endAngle)) points.push(p);
|
|
443
|
+
collectLabel(step, (t) => arcSegmentSample(center, step.radius, step.startAngle, step.endAngle, t));
|
|
444
|
+
penOverride = endPt;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (step.kind === "circlePath") {
|
|
448
|
+
const center = prev.anchor;
|
|
449
|
+
const r = step.radius;
|
|
450
|
+
const right = [center[0] + r, center[1]];
|
|
451
|
+
const left = [center[0] - r, center[1]];
|
|
452
|
+
startSegment(right);
|
|
453
|
+
emitA(r, r, 0, 1, left);
|
|
454
|
+
emitA(r, r, 0, 1, right);
|
|
455
|
+
points.push([center[0] + r, center[1]]);
|
|
456
|
+
points.push([center[0] - r, center[1]]);
|
|
457
|
+
points.push([center[0], center[1] + r]);
|
|
458
|
+
points.push([center[0], center[1] - r]);
|
|
459
|
+
collectLabel(step, (t) => circleSegmentSample(center, r, t));
|
|
460
|
+
penOverride = center;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (step.kind === "ellipsePath") {
|
|
464
|
+
const center = prev.anchor;
|
|
465
|
+
const rx = step.radiusX;
|
|
466
|
+
const ry = step.radiusY;
|
|
467
|
+
const right = [center[0] + rx, center[1]];
|
|
468
|
+
const left = [center[0] - rx, center[1]];
|
|
469
|
+
startSegment(right);
|
|
470
|
+
emitA(rx, ry, 0, 1, left);
|
|
471
|
+
emitA(rx, ry, 0, 1, right);
|
|
472
|
+
points.push([center[0] + rx, center[1]]);
|
|
473
|
+
points.push([center[0] - rx, center[1]]);
|
|
474
|
+
points.push([center[0], center[1] + ry]);
|
|
475
|
+
points.push([center[0], center[1] - ry]);
|
|
476
|
+
collectLabel(step, (t) => ellipseSegmentSample(center, rx, ry, t));
|
|
477
|
+
penOverride = center;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
202
480
|
const currAnchor = anchors[i];
|
|
203
481
|
if (!currAnchor) return null;
|
|
482
|
+
const usedOverride = penOverride;
|
|
483
|
+
penOverride = null;
|
|
204
484
|
if (step.kind === "line") {
|
|
205
|
-
const fromClip = clipForTarget(prev.step.to, currAnchor, nodeIndex);
|
|
485
|
+
const fromClip = usedOverride ?? clipForTarget(prev.step.to, currAnchor, nodeIndex);
|
|
206
486
|
const toClip = clipForTarget(step.to, prev.anchor, nodeIndex);
|
|
207
487
|
if (!fromClip || !toClip) return null;
|
|
208
488
|
startSegment(fromClip);
|
|
209
489
|
emitL(toClip);
|
|
490
|
+
collectLabel(step, (t) => lineSegmentSample(fromClip, toClip, t));
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (step.kind === "curve") {
|
|
494
|
+
const fromClip = usedOverride ?? clipForTarget(prev.step.to, step.control, nodeIndex);
|
|
495
|
+
const toClip = clipForTarget(step.to, step.control, nodeIndex);
|
|
496
|
+
if (!fromClip || !toClip) return null;
|
|
497
|
+
startSegment(fromClip);
|
|
498
|
+
emitQ(step.control, toClip);
|
|
499
|
+
collectLabel(step, (t) => quadSegmentSample(fromClip, step.control, toClip, t));
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (step.kind === "cubic") {
|
|
503
|
+
const fromClip = usedOverride ?? clipForTarget(prev.step.to, step.control1, nodeIndex);
|
|
504
|
+
const toClip = clipForTarget(step.to, step.control2, nodeIndex);
|
|
505
|
+
if (!fromClip || !toClip) return null;
|
|
506
|
+
startSegment(fromClip);
|
|
507
|
+
emitC(step.control1, step.control2, toClip);
|
|
508
|
+
collectLabel(step, (t) => cubicSegmentSample(fromClip, step.control1, step.control2, toClip, t));
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (step.kind === "bend") {
|
|
512
|
+
const angle = step.bendAngle ?? 30;
|
|
513
|
+
const [c1, c2] = bendControlPoints(prev.anchor, currAnchor, step.bendDirection, angle);
|
|
514
|
+
const fromClip = usedOverride ?? clipForTarget(prev.step.to, c1, nodeIndex);
|
|
515
|
+
const toClip = clipForTarget(step.to, c2, nodeIndex);
|
|
516
|
+
if (!fromClip || !toClip) return null;
|
|
517
|
+
startSegment(fromClip);
|
|
518
|
+
emitC(c1, c2, toClip);
|
|
519
|
+
collectLabel(step, (t) => cubicSegmentSample(fromClip, c1, c2, toClip, t));
|
|
210
520
|
continue;
|
|
211
521
|
}
|
|
212
522
|
const corner = cornerOf(prev.anchor, currAnchor, step.via);
|
|
213
|
-
const fromClip = clipForTarget(prev.step.to, corner, nodeIndex);
|
|
523
|
+
const fromClip = usedOverride ?? clipForTarget(prev.step.to, corner, nodeIndex);
|
|
214
524
|
const toClip = clipForTarget(step.to, corner, nodeIndex);
|
|
215
525
|
if (!fromClip || !toClip) return null;
|
|
216
526
|
startSegment(fromClip);
|
|
217
527
|
emitL(corner);
|
|
218
528
|
emitL(toClip);
|
|
529
|
+
collectLabel(step, (t) => foldSegmentSample(fromClip, corner, toClip, t));
|
|
219
530
|
}
|
|
220
|
-
const strokeWidth = path.strokeWidth ?? 1;
|
|
531
|
+
const strokeWidth = path.strokeWidth ?? (path.thickness ? THICKNESS_TO_WIDTH[path.thickness] : 1);
|
|
221
532
|
const baseProps = {
|
|
222
533
|
stroke: path.stroke ?? "currentColor",
|
|
223
534
|
strokeWidth,
|
|
224
535
|
fill: path.fill ?? "none",
|
|
225
536
|
fillRule: path.fillRule,
|
|
226
|
-
strokeDasharray: path.strokeDasharray
|
|
537
|
+
strokeDasharray: path.strokeDasharray,
|
|
538
|
+
strokeLinecap: path.lineCap,
|
|
539
|
+
strokeLinejoin: path.lineJoin,
|
|
540
|
+
opacity: path.opacity,
|
|
541
|
+
fillOpacity: path.fillOpacity,
|
|
542
|
+
strokeOpacity: path.drawOpacity
|
|
227
543
|
};
|
|
228
544
|
const markers = arrowMarkers(path.arrow, path.arrowShape);
|
|
229
545
|
const hasArrows = !!markers.arrowStart || !!markers.arrowEnd;
|
|
@@ -255,6 +571,9 @@ var emitPathPrimitive = (path, nodeIndex, round) => {
|
|
|
255
571
|
}
|
|
256
572
|
const tokens = ops.map((op) => {
|
|
257
573
|
if (op.cmd === "Z") return "Z";
|
|
574
|
+
if (op.cmd === "Q") return `Q ${round(op.control[0])} ${round(op.control[1])} ${round(op.point[0])} ${round(op.point[1])}`;
|
|
575
|
+
if (op.cmd === "C") return `C ${round(op.control1[0])} ${round(op.control1[1])} ${round(op.control2[0])} ${round(op.control2[1])} ${round(op.point[0])} ${round(op.point[1])}`;
|
|
576
|
+
if (op.cmd === "A") return `A ${round(op.rx)} ${round(op.ry)} 0 ${op.largeArc} ${op.sweep} ${round(op.point[0])} ${round(op.point[1])}`;
|
|
258
577
|
return `${op.cmd} ${round(op.point[0])} ${round(op.point[1])}`;
|
|
259
578
|
});
|
|
260
579
|
const subPathStarts = [];
|
|
@@ -262,12 +581,12 @@ var emitPathPrimitive = (path, nodeIndex, round) => {
|
|
|
262
581
|
if (tok.startsWith("M ")) subPathStarts.push(idx);
|
|
263
582
|
});
|
|
264
583
|
if (!hasArrows || subPathStarts.length <= 1) return {
|
|
265
|
-
|
|
584
|
+
primitives: [{
|
|
266
585
|
type: "path",
|
|
267
586
|
d: tokens.join(" "),
|
|
268
587
|
...baseProps,
|
|
269
588
|
...markers
|
|
270
|
-
},
|
|
589
|
+
}, ...labelPrims],
|
|
271
590
|
points
|
|
272
591
|
};
|
|
273
592
|
const subPathSlices = [];
|
|
@@ -277,7 +596,7 @@ var emitPathPrimitive = (path, nodeIndex, round) => {
|
|
|
277
596
|
subPathSlices.push(tokens.slice(start, end));
|
|
278
597
|
}
|
|
279
598
|
return {
|
|
280
|
-
|
|
599
|
+
primitives: [{
|
|
281
600
|
type: "group",
|
|
282
601
|
children: subPathSlices.map((sub, i) => {
|
|
283
602
|
const isFirst = i === 0;
|
|
@@ -290,7 +609,7 @@ var emitPathPrimitive = (path, nodeIndex, round) => {
|
|
|
290
609
|
...isLast && markers.arrowEnd ? { arrowEnd: markers.arrowEnd } : {}
|
|
291
610
|
};
|
|
292
611
|
})
|
|
293
|
-
},
|
|
612
|
+
}, ...labelPrims],
|
|
294
613
|
points
|
|
295
614
|
};
|
|
296
615
|
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { IRPosition, PolarPosition } from '../ir';
|
|
1
|
+
import { IRAtPosition, IRPosition, PolarPosition } from '../ir';
|
|
2
2
|
import { NodeLayout } from './node';
|
|
3
3
|
/**
|
|
4
|
-
* 把 IR 里出现的位置形态(笛卡尔 / 极坐标 / 节点 id)解析为笛卡尔位置。
|
|
5
|
-
* 极坐标的 origin 可递归 / 可引用节点 id
|
|
6
|
-
*
|
|
4
|
+
* 把 IR 里出现的位置形态(笛卡尔 / 极坐标 / 相对定位 / 节点 id)解析为笛卡尔位置。
|
|
5
|
+
* - 极坐标的 origin 可递归 / 可引用节点 id
|
|
6
|
+
* - 相对定位的 of 必须引用已定义的节点 / coordinate(前向引用要求被引节点先出现)
|
|
7
|
+
* - 解析失败返回 null(如引用了未定义节点)
|
|
8
|
+
*
|
|
9
|
+
* `nodeDistance` 是 Tikz 容器 prop 注入的相对定位默认距离;AtPosition 自带 distance 时优先用自带值。
|
|
7
10
|
*/
|
|
8
|
-
export declare const resolvePosition: (pos: IRPosition | PolarPosition | string, nodeMap: Map<string, NodeLayout
|
|
11
|
+
export declare const resolvePosition: (pos: IRPosition | PolarPosition | IRAtPosition | string, nodeMap: Map<string, NodeLayout>, nodeDistance?: number) => IRPosition | null;
|
|
9
12
|
//# sourceMappingURL=position.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"position.d.ts","sourceRoot":"","sources":["../../../src/compile/position.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"position.d.ts","sourceRoot":"","sources":["../../../src/compile/position.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAClF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAqBzC;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAC1B,KAAK,UAAU,GAAG,aAAa,GAAG,YAAY,GAAG,MAAM,EACvD,SAAS,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EAChC,eAAc,MAA8B,KAC3C,UAAU,GAAG,IA4Bf,CAAC"}
|
|
@@ -1,19 +1,46 @@
|
|
|
1
1
|
//#region src/compile/position.ts
|
|
2
|
+
/** 默认相对定位距离(user units)——CompileOptions.nodeDistance 未提供时使用 */
|
|
3
|
+
var DEFAULT_NODE_DISTANCE = 1;
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* 8 方向到屏幕坐标系(y 向下)单位向量的映射。
|
|
6
|
+
* 视觉语义:above 在视觉上方(y 减小),below 在视觉下方(y 增大)。
|
|
7
|
+
* 4 对角分量为 1/√2,保证斜向距离与水平 / 垂直距离等长(对角点位于半径 = distance 的圆周上)。
|
|
6
8
|
*/
|
|
7
|
-
var
|
|
9
|
+
var DIRECTION_VECTOR = {
|
|
10
|
+
above: [0, -1],
|
|
11
|
+
below: [0, 1],
|
|
12
|
+
left: [-1, 0],
|
|
13
|
+
right: [1, 0],
|
|
14
|
+
"above-left": [-Math.SQRT1_2, -Math.SQRT1_2],
|
|
15
|
+
"above-right": [Math.SQRT1_2, -Math.SQRT1_2],
|
|
16
|
+
"below-left": [-Math.SQRT1_2, Math.SQRT1_2],
|
|
17
|
+
"below-right": [Math.SQRT1_2, Math.SQRT1_2]
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* 把 IR 里出现的位置形态(笛卡尔 / 极坐标 / 相对定位 / 节点 id)解析为笛卡尔位置。
|
|
21
|
+
* - 极坐标的 origin 可递归 / 可引用节点 id
|
|
22
|
+
* - 相对定位的 of 必须引用已定义的节点 / coordinate(前向引用要求被引节点先出现)
|
|
23
|
+
* - 解析失败返回 null(如引用了未定义节点)
|
|
24
|
+
*
|
|
25
|
+
* `nodeDistance` 是 Tikz 容器 prop 注入的相对定位默认距离;AtPosition 自带 distance 时优先用自带值。
|
|
26
|
+
*/
|
|
27
|
+
var resolvePosition = (pos, nodeMap, nodeDistance = DEFAULT_NODE_DISTANCE) => {
|
|
8
28
|
if (typeof pos === "string") {
|
|
9
29
|
const node = nodeMap.get(pos);
|
|
10
30
|
return node ? [node.rect.x, node.rect.y] : null;
|
|
11
31
|
}
|
|
12
32
|
if (Array.isArray(pos)) return pos;
|
|
33
|
+
if ("direction" in pos) {
|
|
34
|
+
const ref = nodeMap.get(pos.of);
|
|
35
|
+
if (!ref) return null;
|
|
36
|
+
const distance = pos.distance ?? nodeDistance;
|
|
37
|
+
const [dx, dy] = DIRECTION_VECTOR[pos.direction];
|
|
38
|
+
return [ref.rect.x + dx * distance, ref.rect.y + dy * distance];
|
|
39
|
+
}
|
|
13
40
|
let origin;
|
|
14
41
|
if (!pos.origin) origin = [0, 0];
|
|
15
42
|
else {
|
|
16
|
-
const resolved = resolvePosition(pos.origin, nodeMap);
|
|
43
|
+
const resolved = resolvePosition(pos.origin, nodeMap, nodeDistance);
|
|
17
44
|
if (!resolved) return null;
|
|
18
45
|
origin = resolved;
|
|
19
46
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Position } from './point';
|
|
2
|
+
/**
|
|
3
|
+
* 给定圆心、半径、角度(度,与 polar.toPosition 同约定),返回圆周上对应点。
|
|
4
|
+
*/
|
|
5
|
+
export declare const arcEndPoint: (center: Position, radius: number, angleDeg: number) => Position;
|
|
6
|
+
/**
|
|
7
|
+
* 计算 SVG `<path>` A 命令需要的 large-arc-flag 与 sweep-flag。
|
|
8
|
+
*
|
|
9
|
+
* - `largeArc`:弧跨度 `|endAngle - startAngle|` 严格大于 180° 时为 1。
|
|
10
|
+
* - `sweep`:`endAngle >= startAngle` 时为 1,否则为 0;与"角度增加方向 = 在
|
|
11
|
+
* SVG 屏幕上视觉顺时针"一致——因为我们投影时未翻转 y,SVG sweep=1(屏幕 CW)
|
|
12
|
+
* 恰好等价 math 角度增加。
|
|
13
|
+
*
|
|
14
|
+
* 边界:`|Δ|=180°` 时 `largeArc=0`(半弧不算大弧);恰好同点 `|Δ|=0` 也返回 0。
|
|
15
|
+
*/
|
|
16
|
+
export declare const arcSvgFlags: (startAngleDeg: number, endAngleDeg: number) => {
|
|
17
|
+
largeArc: 0 | 1;
|
|
18
|
+
sweep: 0 | 1;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* 弧的 bounding box 极值候选点:起点、终点,以及 `[startAngle, endAngle]` 区间
|
|
22
|
+
* 里所有 90°·k 的基本方向(0°、90°、180°、270° 及其周期延拓)对应的圆周点。
|
|
23
|
+
*
|
|
24
|
+
* 这些点是计算视图框(viewBox)/ bbox 时必须考虑的候选——因为弧线投影到 x/y
|
|
25
|
+
* 轴的极值只可能在弧端点或圆周上的轴向四点处出现。
|
|
26
|
+
*
|
|
27
|
+
* 说明:
|
|
28
|
+
* - 接受 `endAngle < startAngle`(CW math 方向):以 `min..max` 区间扫描 90°·k;
|
|
29
|
+
* 语义上是"无论扫描方向,此弧覆盖到的角度区间内的基本方向都算极值候选"。
|
|
30
|
+
* - 跨 360°(如 270° → 450°)也按数值区间处理,正确收录中间穿越的 0°、90° 等。
|
|
31
|
+
* - 不去重:若起点恰好落在 90°·k 上会同时出现两次,调用方根据需要再处理。
|
|
32
|
+
*/
|
|
33
|
+
export declare const arcBoundingPoints: (center: Position, radius: number, startAngleDeg: number, endAngleDeg: number) => Array<Position>;
|
|
34
|
+
//# sourceMappingURL=arc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arc.d.ts","sourceRoot":"","sources":["../../../src/geometry/arc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAkBxC;;GAEG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,QAAQ,EAChB,QAAQ,MAAM,EACd,UAAU,MAAM,KACf,QAMF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GACtB,eAAe,MAAM,EACrB,aAAa,MAAM,KAClB;IAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAA;CAMjC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,QAAQ,EAChB,QAAQ,MAAM,EACd,eAAe,MAAM,EACrB,aAAa,MAAM,KAClB,KAAK,CAAC,QAAQ,CAmBhB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/geometry/arc.ts
|
|
2
|
+
var DEG_TO_RAD = Math.PI / 180;
|
|
3
|
+
/**
|
|
4
|
+
* 给定圆心、半径、角度(度,与 polar.toPosition 同约定),返回圆周上对应点。
|
|
5
|
+
*/
|
|
6
|
+
var arcEndPoint = (center, radius, angleDeg) => {
|
|
7
|
+
const rad = angleDeg * DEG_TO_RAD;
|
|
8
|
+
return [center[0] + Math.cos(rad) * radius, center[1] + Math.sin(rad) * radius];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* 计算 SVG `<path>` A 命令需要的 large-arc-flag 与 sweep-flag。
|
|
12
|
+
*
|
|
13
|
+
* - `largeArc`:弧跨度 `|endAngle - startAngle|` 严格大于 180° 时为 1。
|
|
14
|
+
* - `sweep`:`endAngle >= startAngle` 时为 1,否则为 0;与"角度增加方向 = 在
|
|
15
|
+
* SVG 屏幕上视觉顺时针"一致——因为我们投影时未翻转 y,SVG sweep=1(屏幕 CW)
|
|
16
|
+
* 恰好等价 math 角度增加。
|
|
17
|
+
*
|
|
18
|
+
* 边界:`|Δ|=180°` 时 `largeArc=0`(半弧不算大弧);恰好同点 `|Δ|=0` 也返回 0。
|
|
19
|
+
*/
|
|
20
|
+
var arcSvgFlags = (startAngleDeg, endAngleDeg) => {
|
|
21
|
+
return {
|
|
22
|
+
largeArc: Math.abs(endAngleDeg - startAngleDeg) > 180 ? 1 : 0,
|
|
23
|
+
sweep: endAngleDeg >= startAngleDeg ? 1 : 0
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* 弧的 bounding box 极值候选点:起点、终点,以及 `[startAngle, endAngle]` 区间
|
|
28
|
+
* 里所有 90°·k 的基本方向(0°、90°、180°、270° 及其周期延拓)对应的圆周点。
|
|
29
|
+
*
|
|
30
|
+
* 这些点是计算视图框(viewBox)/ bbox 时必须考虑的候选——因为弧线投影到 x/y
|
|
31
|
+
* 轴的极值只可能在弧端点或圆周上的轴向四点处出现。
|
|
32
|
+
*
|
|
33
|
+
* 说明:
|
|
34
|
+
* - 接受 `endAngle < startAngle`(CW math 方向):以 `min..max` 区间扫描 90°·k;
|
|
35
|
+
* 语义上是"无论扫描方向,此弧覆盖到的角度区间内的基本方向都算极值候选"。
|
|
36
|
+
* - 跨 360°(如 270° → 450°)也按数值区间处理,正确收录中间穿越的 0°、90° 等。
|
|
37
|
+
* - 不去重:若起点恰好落在 90°·k 上会同时出现两次,调用方根据需要再处理。
|
|
38
|
+
*/
|
|
39
|
+
var arcBoundingPoints = (center, radius, startAngleDeg, endAngleDeg) => {
|
|
40
|
+
const points = [arcEndPoint(center, radius, startAngleDeg), arcEndPoint(center, radius, endAngleDeg)];
|
|
41
|
+
const lo = Math.min(startAngleDeg, endAngleDeg);
|
|
42
|
+
const hi = Math.max(startAngleDeg, endAngleDeg);
|
|
43
|
+
const kStart = Math.ceil(lo / 90);
|
|
44
|
+
const kEnd = Math.floor(hi / 90);
|
|
45
|
+
for (let k = kStart; k <= kEnd; k++) {
|
|
46
|
+
const angle = k * 90;
|
|
47
|
+
if (angle === startAngleDeg || angle === endAngleDeg) continue;
|
|
48
|
+
points.push(arcEndPoint(center, radius, angle));
|
|
49
|
+
}
|
|
50
|
+
return points;
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { arcBoundingPoints, arcEndPoint, arcSvgFlags };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Position } from './point';
|
|
2
|
+
/**
|
|
3
|
+
* 用 cubic Bezier 拟合 from→to 的弧形 bend。
|
|
4
|
+
*
|
|
5
|
+
* 算法(ADR-0001):
|
|
6
|
+
* 1. apex offset = chord × tan(bendAngle / 2)(沿 path 法向 left / right 偏移量)
|
|
7
|
+
* 2. 控制点取 chord 1/3 / 2/3 处沿法向偏移;为让 cubic 在 t=0.5 处穿过 apex,
|
|
8
|
+
* 控制点 offset = (4/3) × apex offset(推导:B(0.5) = midpoint + (3/4)·s·n)
|
|
9
|
+
*
|
|
10
|
+
* 法向定义(SVG y 向下,"left/right" 按视觉,不按数学):
|
|
11
|
+
* chord direction = (dx, dy) / |chord|
|
|
12
|
+
* visual-left normal = ( dy, -dx) / |chord| (从 from 看向 to,向左偏)
|
|
13
|
+
* visual-right normal = (-dy, dx) / |chord|
|
|
14
|
+
*
|
|
15
|
+
* chord 长度为 0 时无方向可推,两个控制点都返回 from。
|
|
16
|
+
*/
|
|
17
|
+
export declare const bendControlPoints: (from: Position, to: Position, direction: "left" | "right", bendAngle: number) => [Position, Position];
|
|
18
|
+
//# sourceMappingURL=bend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bend.d.ts","sourceRoot":"","sources":["../../../src/geometry/bend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAC5B,MAAM,QAAQ,EACd,IAAI,QAAQ,EACZ,WAAW,MAAM,GAAG,OAAO,EAC3B,WAAW,MAAM,KAChB,CAAC,QAAQ,EAAE,QAAQ,CAgBrB,CAAC"}
|