@kgdiff/renderer 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.
- package/LICENSE +21 -0
- package/README.md +53 -2
- package/dist/atlas.d.ts +55 -0
- package/dist/atlas.d.ts.map +1 -0
- package/dist/atlas.js +132 -0
- package/dist/atlas.js.map +1 -0
- package/dist/camera.d.ts +116 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +368 -0
- package/dist/camera.js.map +1 -0
- package/dist/color.d.ts +12 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +51 -0
- package/dist/color.js.map +1 -0
- package/dist/ctx.d.ts +57 -0
- package/dist/ctx.d.ts.map +1 -0
- package/dist/ctx.js +28 -0
- package/dist/ctx.js.map +1 -0
- package/dist/draw/background.d.ts +33 -0
- package/dist/draw/background.d.ts.map +1 -0
- package/dist/draw/background.js +105 -0
- package/dist/draw/background.js.map +1 -0
- package/dist/draw/badges.d.ts +26 -0
- package/dist/draw/badges.d.ts.map +1 -0
- package/dist/draw/badges.js +82 -0
- package/dist/draw/badges.js.map +1 -0
- package/dist/draw/collapsed.d.ts +15 -0
- package/dist/draw/collapsed.d.ts.map +1 -0
- package/dist/draw/collapsed.js +67 -0
- package/dist/draw/collapsed.js.map +1 -0
- package/dist/draw/edges.d.ts +14 -0
- package/dist/draw/edges.d.ts.map +1 -0
- package/dist/draw/edges.js +115 -0
- package/dist/draw/edges.js.map +1 -0
- package/dist/draw/glyphs.d.ts +31 -0
- package/dist/draw/glyphs.d.ts.map +1 -0
- package/dist/draw/glyphs.js +114 -0
- package/dist/draw/glyphs.js.map +1 -0
- package/dist/draw/halos.d.ts +19 -0
- package/dist/draw/halos.d.ts.map +1 -0
- package/dist/draw/halos.js +91 -0
- package/dist/draw/halos.js.map +1 -0
- package/dist/draw/labels.d.ts +55 -0
- package/dist/draw/labels.d.ts.map +1 -0
- package/dist/draw/labels.js +300 -0
- package/dist/draw/labels.js.map +1 -0
- package/dist/draw/link.d.ts +25 -0
- package/dist/draw/link.d.ts.map +1 -0
- package/dist/draw/link.js +122 -0
- package/dist/draw/link.js.map +1 -0
- package/dist/draw/nodes.d.ts +13 -0
- package/dist/draw/nodes.d.ts.map +1 -0
- package/dist/draw/nodes.js +165 -0
- package/dist/draw/nodes.js.map +1 -0
- package/dist/draw/shapes.d.ts +7 -0
- package/dist/draw/shapes.d.ts.map +1 -0
- package/dist/draw/shapes.js +26 -0
- package/dist/draw/shapes.js.map +1 -0
- package/dist/draw/view.d.ts +36 -0
- package/dist/draw/view.d.ts.map +1 -0
- package/dist/draw/view.js +16 -0
- package/dist/draw/view.js.map +1 -0
- package/dist/frame-cache.d.ts +103 -0
- package/dist/frame-cache.d.ts.map +1 -0
- package/dist/frame-cache.js +349 -0
- package/dist/frame-cache.js.map +1 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +81 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +329 -0
- package/dist/input.js.map +1 -0
- package/dist/labels.d.ts +103 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +203 -0
- package/dist/labels.js.map +1 -0
- package/dist/math.d.ts +33 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +138 -0
- package/dist/math.js.map +1 -0
- package/dist/minimap.d.ts +56 -0
- package/dist/minimap.d.ts.map +1 -0
- package/dist/minimap.js +501 -0
- package/dist/minimap.js.map +1 -0
- package/dist/paint-stats.d.ts +17 -0
- package/dist/paint-stats.d.ts.map +1 -0
- package/dist/paint-stats.js +23 -0
- package/dist/paint-stats.js.map +1 -0
- package/dist/paint.d.ts +64 -0
- package/dist/paint.d.ts.map +1 -0
- package/dist/paint.js +197 -0
- package/dist/paint.js.map +1 -0
- package/dist/pick.d.ts +26 -0
- package/dist/pick.d.ts.map +1 -0
- package/dist/pick.js +125 -0
- package/dist/pick.js.map +1 -0
- package/dist/renderer.d.ts +28 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +470 -0
- package/dist/renderer.js.map +1 -0
- package/dist/smooth-zoom.d.ts +21 -0
- package/dist/smooth-zoom.d.ts.map +1 -0
- package/dist/smooth-zoom.js +52 -0
- package/dist/smooth-zoom.js.map +1 -0
- package/dist/spatial.d.ts +26 -0
- package/dist/spatial.d.ts.map +1 -0
- package/dist/spatial.js +103 -0
- package/dist/spatial.js.map +1 -0
- package/dist/text.d.ts +36 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +123 -0
- package/dist/text.js.map +1 -0
- package/package.json +47 -3
- package/src/atlas.ts +171 -0
- package/src/camera.ts +482 -0
- package/src/color.ts +58 -0
- package/src/ctx.ts +102 -0
- package/src/draw/background.ts +139 -0
- package/src/draw/badges.ts +93 -0
- package/src/draw/collapsed.ts +86 -0
- package/src/draw/edges.ts +135 -0
- package/src/draw/glyphs.ts +168 -0
- package/src/draw/halos.ts +101 -0
- package/src/draw/labels.ts +399 -0
- package/src/draw/link.ts +157 -0
- package/src/draw/nodes.ts +184 -0
- package/src/draw/shapes.ts +36 -0
- package/src/draw/view.ts +37 -0
- package/src/frame-cache.ts +382 -0
- package/src/index.ts +161 -0
- package/src/input.ts +370 -0
- package/src/labels.ts +269 -0
- package/src/math.ts +185 -0
- package/src/minimap.ts +545 -0
- package/src/paint-stats.ts +38 -0
- package/src/paint.ts +235 -0
- package/src/pick.ts +173 -0
- package/src/renderer.ts +539 -0
- package/src/smooth-zoom.ts +65 -0
- package/src/spatial.ts +94 -0
- package/src/text.ts +130 -0
package/src/labels.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Greedy label placement in screen space (docs/visual-language.md §Level of detail).
|
|
3
|
+
*
|
|
4
|
+
* Labels are placed by priority (desc; ties by `order`, so placement is deterministic). Each
|
|
5
|
+
* tries its candidate positions in order and takes the first that does not overlap anything
|
|
6
|
+
* placed before it (labels, badges, node discs). Overlap tests use a uniform grid, so a frame
|
|
7
|
+
* with n labels costs O(n) expected.
|
|
8
|
+
*/
|
|
9
|
+
import type { LabelVisual } from '@kgdiff/scene';
|
|
10
|
+
import { rectsOverlap } from './math.js';
|
|
11
|
+
|
|
12
|
+
/** Gap (px) between a node's edge and a label beside it. */
|
|
13
|
+
export const LABEL_SIDE_GAP = 6;
|
|
14
|
+
/** Breathing room (px) kept between placed boxes. */
|
|
15
|
+
export const LABEL_PAD = 2;
|
|
16
|
+
|
|
17
|
+
/** Candidate sides. */
|
|
18
|
+
export const SIDE_BELOW = 0;
|
|
19
|
+
export const SIDE_RIGHT = 1;
|
|
20
|
+
export const SIDE_ABOVE = 2;
|
|
21
|
+
export const SIDE_LEFT = 3;
|
|
22
|
+
export const SIDE_CENTER = 4;
|
|
23
|
+
|
|
24
|
+
/** Node labels try below, right, above, left. */
|
|
25
|
+
export const MODE_NODE = 0;
|
|
26
|
+
/** Edge / glyph labels try centred on the anchor, then above, then below it. */
|
|
27
|
+
export const MODE_POINT = 1;
|
|
28
|
+
/** Fixed boxes (collapsed pills): centred on the anchor or not at all. */
|
|
29
|
+
export const MODE_FIXED = 2;
|
|
30
|
+
|
|
31
|
+
/** One label block (a primary line with an optional secondary line under it). */
|
|
32
|
+
export interface LabelRequest {
|
|
33
|
+
/** Anchor (node centre or edge midpoint), css px. */
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
/** Clearance around the anchor (node radius on screen), px. */
|
|
37
|
+
r: number;
|
|
38
|
+
/** Block size, px. */
|
|
39
|
+
w: number;
|
|
40
|
+
h: number;
|
|
41
|
+
/** Distance below the node edge for the `below` candidate (and above for `above`), px. */
|
|
42
|
+
offset: number;
|
|
43
|
+
priority: number;
|
|
44
|
+
/** Tie-breaker: lower wins. */
|
|
45
|
+
order: number;
|
|
46
|
+
mode: number;
|
|
47
|
+
/** Collapsed-group index for pills, -1 for labels. */
|
|
48
|
+
pill: number;
|
|
49
|
+
// ── output ──
|
|
50
|
+
placed: boolean;
|
|
51
|
+
side: number;
|
|
52
|
+
bx: number;
|
|
53
|
+
by: number;
|
|
54
|
+
// ── payload for drawing (opaque to the placer) ──
|
|
55
|
+
alpha: number;
|
|
56
|
+
primary: LabelVisual | null;
|
|
57
|
+
secondary: LabelVisual | null;
|
|
58
|
+
textP: string;
|
|
59
|
+
textS: string;
|
|
60
|
+
fontP: string;
|
|
61
|
+
fontS: string;
|
|
62
|
+
wP: number;
|
|
63
|
+
wS: number;
|
|
64
|
+
hP: number;
|
|
65
|
+
hS: number;
|
|
66
|
+
/** Offset of the secondary line from the block top, px. */
|
|
67
|
+
dyS: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createRequest(): LabelRequest {
|
|
71
|
+
return {
|
|
72
|
+
x: 0,
|
|
73
|
+
y: 0,
|
|
74
|
+
r: 0,
|
|
75
|
+
w: 0,
|
|
76
|
+
h: 0,
|
|
77
|
+
offset: 0,
|
|
78
|
+
priority: 0,
|
|
79
|
+
order: 0,
|
|
80
|
+
mode: MODE_NODE,
|
|
81
|
+
pill: -1,
|
|
82
|
+
placed: false,
|
|
83
|
+
side: SIDE_BELOW,
|
|
84
|
+
bx: 0,
|
|
85
|
+
by: 0,
|
|
86
|
+
alpha: 1,
|
|
87
|
+
primary: null,
|
|
88
|
+
secondary: null,
|
|
89
|
+
textP: '',
|
|
90
|
+
textS: '',
|
|
91
|
+
fontP: '',
|
|
92
|
+
fontS: '',
|
|
93
|
+
wP: 0,
|
|
94
|
+
wS: 0,
|
|
95
|
+
hP: 0,
|
|
96
|
+
hS: 0,
|
|
97
|
+
dyS: 0,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Region labels must stay in. With `strictX`, boxes may not cross x0 / x1 (swipe divider). */
|
|
102
|
+
export interface LabelClip {
|
|
103
|
+
x0: number;
|
|
104
|
+
y0: number;
|
|
105
|
+
x1: number;
|
|
106
|
+
y1: number;
|
|
107
|
+
strictX: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Uniform-grid index of occupied screen rectangles. */
|
|
111
|
+
export class CollisionGrid {
|
|
112
|
+
private cell = 64;
|
|
113
|
+
private cols = 1;
|
|
114
|
+
private rows = 1;
|
|
115
|
+
private x0 = 0;
|
|
116
|
+
private y0 = 0;
|
|
117
|
+
private cells: number[][] = [[]];
|
|
118
|
+
private touched: number[] = [];
|
|
119
|
+
private rects = new Float64Array(4 * 256);
|
|
120
|
+
private n = 0;
|
|
121
|
+
|
|
122
|
+
/** Clear and cover [x0, x0 + width] × [y0, y0 + height] with `cell`-px cells. */
|
|
123
|
+
reset(x0: number, y0: number, width: number, height: number, cell = 64): void {
|
|
124
|
+
for (const c of this.touched) this.cells[c]!.length = 0;
|
|
125
|
+
this.touched.length = 0;
|
|
126
|
+
this.n = 0;
|
|
127
|
+
this.cell = cell;
|
|
128
|
+
this.x0 = x0;
|
|
129
|
+
this.y0 = y0;
|
|
130
|
+
this.cols = Math.max(1, Math.ceil(width / cell));
|
|
131
|
+
this.rows = Math.max(1, Math.ceil(height / cell));
|
|
132
|
+
const need = this.cols * this.rows;
|
|
133
|
+
while (this.cells.length < need) this.cells.push([]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get count(): number {
|
|
137
|
+
return this.n;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
insert(x: number, y: number, w: number, h: number): void {
|
|
141
|
+
if (this.rects.length < (this.n + 1) * 4) {
|
|
142
|
+
const next = new Float64Array(this.rects.length * 2);
|
|
143
|
+
next.set(this.rects);
|
|
144
|
+
this.rects = next;
|
|
145
|
+
}
|
|
146
|
+
const id = this.n++;
|
|
147
|
+
const o = id * 4;
|
|
148
|
+
this.rects[o] = x;
|
|
149
|
+
this.rects[o + 1] = y;
|
|
150
|
+
this.rects[o + 2] = w;
|
|
151
|
+
this.rects[o + 3] = h;
|
|
152
|
+
const c0 = this.col(x);
|
|
153
|
+
const c1 = this.col(x + w);
|
|
154
|
+
const r0 = this.row(y);
|
|
155
|
+
const r1 = this.row(y + h);
|
|
156
|
+
for (let r = r0; r <= r1; r++) {
|
|
157
|
+
for (let c = c0; c <= c1; c++) {
|
|
158
|
+
const idx = r * this.cols + c;
|
|
159
|
+
const list = this.cells[idx]!;
|
|
160
|
+
if (list.length === 0) this.touched.push(idx);
|
|
161
|
+
list.push(id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
overlaps(x: number, y: number, w: number, h: number): boolean {
|
|
167
|
+
const c0 = this.col(x);
|
|
168
|
+
const c1 = this.col(x + w);
|
|
169
|
+
const r0 = this.row(y);
|
|
170
|
+
const r1 = this.row(y + h);
|
|
171
|
+
const rects = this.rects;
|
|
172
|
+
for (let r = r0; r <= r1; r++) {
|
|
173
|
+
for (let c = c0; c <= c1; c++) {
|
|
174
|
+
const list = this.cells[r * this.cols + c]!;
|
|
175
|
+
for (let i = 0; i < list.length; i++) {
|
|
176
|
+
const o = list[i]! * 4;
|
|
177
|
+
if (rectsOverlap(x, y, w, h, rects[o]!, rects[o + 1]!, rects[o + 2]!, rects[o + 3]!)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private col(x: number): number {
|
|
187
|
+
const c = Math.floor((x - this.x0) / this.cell);
|
|
188
|
+
return c < 0 ? 0 : c >= this.cols ? this.cols - 1 : c;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private row(y: number): number {
|
|
192
|
+
const r = Math.floor((y - this.y0) / this.cell);
|
|
193
|
+
return r < 0 ? 0 : r >= this.rows ? this.rows - 1 : r;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function byPriority(a: LabelRequest, b: LabelRequest): number {
|
|
198
|
+
return b.priority - a.priority || a.order - b.order;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const NODE_SIDES = [SIDE_BELOW, SIDE_RIGHT, SIDE_ABOVE, SIDE_LEFT] as const;
|
|
202
|
+
const POINT_SIDES = [SIDE_CENTER, SIDE_ABOVE, SIDE_BELOW] as const;
|
|
203
|
+
const FIXED_SIDES = [SIDE_CENTER] as const;
|
|
204
|
+
|
|
205
|
+
/** Top-left of a candidate box (written by `candidateBox`, so placement never allocates). */
|
|
206
|
+
export const candidate = { x: 0, y: 0 };
|
|
207
|
+
|
|
208
|
+
/** Top-left of `req`'s box for a candidate side, written into `candidate`. */
|
|
209
|
+
export function candidateBox(req: LabelRequest, side: number): typeof candidate {
|
|
210
|
+
const { x, y, r, w, h } = req;
|
|
211
|
+
let bx: number;
|
|
212
|
+
let by: number;
|
|
213
|
+
if (req.mode === MODE_FIXED) {
|
|
214
|
+
bx = x - w / 2;
|
|
215
|
+
by = y - h / 2;
|
|
216
|
+
} else if (req.mode === MODE_POINT) {
|
|
217
|
+
bx = x - w / 2;
|
|
218
|
+
by = side === SIDE_ABOVE ? y - h - 4 : side === SIDE_BELOW ? y + 4 : y - h / 2;
|
|
219
|
+
} else if (side === SIDE_RIGHT) {
|
|
220
|
+
bx = x + r + LABEL_SIDE_GAP;
|
|
221
|
+
by = y - h / 2;
|
|
222
|
+
} else if (side === SIDE_ABOVE) {
|
|
223
|
+
bx = x - w / 2;
|
|
224
|
+
by = y - r - req.offset - h;
|
|
225
|
+
} else if (side === SIDE_LEFT) {
|
|
226
|
+
bx = x - r - LABEL_SIDE_GAP - w;
|
|
227
|
+
by = y - h / 2;
|
|
228
|
+
} else {
|
|
229
|
+
bx = x - w / 2;
|
|
230
|
+
by = y + r + req.offset;
|
|
231
|
+
}
|
|
232
|
+
candidate.x = bx;
|
|
233
|
+
candidate.y = by;
|
|
234
|
+
return candidate;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Place `requests` (sorted in place by priority) against `grid`, which may already hold
|
|
239
|
+
* obstacles. Sets `placed`, `side`, `bx`, `by` on each request and inserts placed boxes.
|
|
240
|
+
* Returns the number of labels placed.
|
|
241
|
+
*/
|
|
242
|
+
export function placeLabels(
|
|
243
|
+
requests: LabelRequest[],
|
|
244
|
+
grid: CollisionGrid,
|
|
245
|
+
clip: LabelClip,
|
|
246
|
+
): number {
|
|
247
|
+
requests.sort(byPriority);
|
|
248
|
+
let placed = 0;
|
|
249
|
+
for (const req of requests) {
|
|
250
|
+
req.placed = false;
|
|
251
|
+
const sides =
|
|
252
|
+
req.mode === MODE_FIXED ? FIXED_SIDES : req.mode === MODE_POINT ? POINT_SIDES : NODE_SIDES;
|
|
253
|
+
for (const side of sides) {
|
|
254
|
+
const { x: bx, y: by } = candidateBox(req, side);
|
|
255
|
+
const { w, h } = req;
|
|
256
|
+
if (bx + w < clip.x0 || bx > clip.x1 || by + h < clip.y0 || by > clip.y1) continue;
|
|
257
|
+
if (clip.strictX && (bx < clip.x0 || bx + w > clip.x1)) continue;
|
|
258
|
+
if (grid.overlaps(bx, by, w, h)) continue;
|
|
259
|
+
grid.insert(bx - LABEL_PAD, by - LABEL_PAD, w + 2 * LABEL_PAD, h + 2 * LABEL_PAD);
|
|
260
|
+
req.placed = true;
|
|
261
|
+
req.side = side;
|
|
262
|
+
req.bx = bx;
|
|
263
|
+
req.by = by;
|
|
264
|
+
placed++;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return placed;
|
|
269
|
+
}
|
package/src/math.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** Small numeric helpers shared by the camera, drawing, culling and picking. */
|
|
2
|
+
|
|
3
|
+
export const TAU = Math.PI * 2;
|
|
4
|
+
|
|
5
|
+
/** `value` limited to [lo, hi] (NaN stays NaN). */
|
|
6
|
+
export function clamp(value: number, lo: number, hi: number): number {
|
|
7
|
+
return Math.min(Math.max(value, lo), hi);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function lerp(a: number, b: number, t: number): number {
|
|
11
|
+
return a + (b - a) * t;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Hermite smoothstep of `x` between `edge0` and `edge1`. */
|
|
15
|
+
export function smoothstep(edge0: number, edge1: number, x: number): number {
|
|
16
|
+
const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
|
|
17
|
+
return t * t * (3 - 2 * t);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function easeInOutCubic(t: number): number {
|
|
21
|
+
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Round a CSS-pixel coordinate to the device-pixel grid (crisp text and hairlines). */
|
|
25
|
+
export function snap(value: number, dpr: number): number {
|
|
26
|
+
return Math.round(value * dpr) / dpr;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Offset (css px) that puts a stroke of `width` css px on whole device pixels:
|
|
31
|
+
* odd device widths are centred on pixel centres, even ones on pixel edges.
|
|
32
|
+
*/
|
|
33
|
+
export function crispOffset(width: number, dpr: number): number {
|
|
34
|
+
return Math.round(width * dpr) % 2 === 1 ? 0.5 / dpr : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Quadratic Bézier edges ─────────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// Curvature convention (edges and glyphs): the control point is the chord midpoint
|
|
40
|
+
// moved along the left normal (−dy, dx) by `curvature · chord length`. The curve's
|
|
41
|
+
// apex therefore sits `curvature / 2 · length` off the chord; the sign picks the side.
|
|
42
|
+
|
|
43
|
+
/** One coordinate of B(t) for a quadratic with endpoints `p0`, `p2` and control `c`. */
|
|
44
|
+
export function quadAt(p0: number, c: number, p2: number, t: number): number {
|
|
45
|
+
const u = 1 - t;
|
|
46
|
+
return u * u * p0 + 2 * u * t * c + t * t * p2;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One coordinate of B′(t). */
|
|
50
|
+
export function quadSlope(p0: number, c: number, p2: number, t: number): number {
|
|
51
|
+
return 2 * (1 - t) * (c - p0) + 2 * t * (p2 - c);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parameter where the curve is `dist` away from its start (`fromEnd` false) or end
|
|
56
|
+
* (`fromEnd` true). Bisection, valid for the gentle curvatures used for parallel edges.
|
|
57
|
+
*/
|
|
58
|
+
export function quadTrim(
|
|
59
|
+
x0: number,
|
|
60
|
+
y0: number,
|
|
61
|
+
cx: number,
|
|
62
|
+
cy: number,
|
|
63
|
+
x2: number,
|
|
64
|
+
y2: number,
|
|
65
|
+
dist: number,
|
|
66
|
+
fromEnd: boolean,
|
|
67
|
+
): number {
|
|
68
|
+
if (dist <= 0) return fromEnd ? 1 : 0;
|
|
69
|
+
const ox = fromEnd ? x2 : x0;
|
|
70
|
+
const oy = fromEnd ? y2 : y0;
|
|
71
|
+
const d2 = dist * dist;
|
|
72
|
+
let lo = fromEnd ? 1 : 0;
|
|
73
|
+
let hi = 0.5;
|
|
74
|
+
const mx = quadAt(x0, cx, x2, 0.5) - ox;
|
|
75
|
+
const my = quadAt(y0, cy, y2, 0.5) - oy;
|
|
76
|
+
if (mx * mx + my * my <= d2) return 0.5;
|
|
77
|
+
for (let i = 0; i < 12; i++) {
|
|
78
|
+
const mid = (lo + hi) / 2;
|
|
79
|
+
const px = quadAt(x0, cx, x2, mid) - ox;
|
|
80
|
+
const py = quadAt(y0, cy, y2, mid) - oy;
|
|
81
|
+
if (px * px + py * py < d2) lo = mid;
|
|
82
|
+
else hi = mid;
|
|
83
|
+
}
|
|
84
|
+
return (lo + hi) / 2;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Squared distance from (px, py) to the segment a–b. */
|
|
88
|
+
export function distToSegmentSq(
|
|
89
|
+
px: number,
|
|
90
|
+
py: number,
|
|
91
|
+
ax: number,
|
|
92
|
+
ay: number,
|
|
93
|
+
bx: number,
|
|
94
|
+
by: number,
|
|
95
|
+
): number {
|
|
96
|
+
const dx = bx - ax;
|
|
97
|
+
const dy = by - ay;
|
|
98
|
+
const len2 = dx * dx + dy * dy;
|
|
99
|
+
let t = len2 > 0 ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0;
|
|
100
|
+
t = clamp(t, 0, 1);
|
|
101
|
+
const ex = ax + t * dx - px;
|
|
102
|
+
const ey = ay + t * dy - py;
|
|
103
|
+
return ex * ex + ey * ey;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Squared distance from (px, py) to a quadratic curve, by flattening into `steps` segments. */
|
|
107
|
+
export function distToQuadSq(
|
|
108
|
+
px: number,
|
|
109
|
+
py: number,
|
|
110
|
+
x0: number,
|
|
111
|
+
y0: number,
|
|
112
|
+
cx: number,
|
|
113
|
+
cy: number,
|
|
114
|
+
x2: number,
|
|
115
|
+
y2: number,
|
|
116
|
+
steps = 16,
|
|
117
|
+
): number {
|
|
118
|
+
let best = Number.POSITIVE_INFINITY;
|
|
119
|
+
let ax = x0;
|
|
120
|
+
let ay = y0;
|
|
121
|
+
for (let i = 1; i <= steps; i++) {
|
|
122
|
+
const t = i / steps;
|
|
123
|
+
const bx = quadAt(x0, cx, x2, t);
|
|
124
|
+
const by = quadAt(y0, cy, y2, t);
|
|
125
|
+
const d = distToSegmentSq(px, py, ax, ay, bx, by);
|
|
126
|
+
if (d < best) best = d;
|
|
127
|
+
ax = bx;
|
|
128
|
+
ay = by;
|
|
129
|
+
}
|
|
130
|
+
return best;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Whether segment a–b touches the axis-aligned rectangle (Liang–Barsky). */
|
|
134
|
+
export function segmentHitsRect(
|
|
135
|
+
ax: number,
|
|
136
|
+
ay: number,
|
|
137
|
+
bx: number,
|
|
138
|
+
by: number,
|
|
139
|
+
x0: number,
|
|
140
|
+
y0: number,
|
|
141
|
+
x1: number,
|
|
142
|
+
y1: number,
|
|
143
|
+
): boolean {
|
|
144
|
+
if (ax >= x0 && ax <= x1 && ay >= y0 && ay <= y1) return true;
|
|
145
|
+
if (bx >= x0 && bx <= x1 && by >= y0 && by <= y1) return true;
|
|
146
|
+
if (
|
|
147
|
+
(ax < x0 && bx < x0) ||
|
|
148
|
+
(ax > x1 && bx > x1) ||
|
|
149
|
+
(ay < y0 && by < y0) ||
|
|
150
|
+
(ay > y1 && by > y1)
|
|
151
|
+
) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const dx = bx - ax;
|
|
155
|
+
const dy = by - ay;
|
|
156
|
+
let t0 = 0;
|
|
157
|
+
let t1 = 1;
|
|
158
|
+
const clip = (p: number, q: number): boolean => {
|
|
159
|
+
if (p === 0) return q >= 0;
|
|
160
|
+
const r = q / p;
|
|
161
|
+
if (p < 0) {
|
|
162
|
+
if (r > t1) return false;
|
|
163
|
+
if (r > t0) t0 = r;
|
|
164
|
+
} else {
|
|
165
|
+
if (r < t0) return false;
|
|
166
|
+
if (r < t1) t1 = r;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
};
|
|
170
|
+
return clip(-dx, ax - x0) && clip(dx, x1 - ax) && clip(-dy, ay - y0) && clip(dy, y1 - ay);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Whether two rectangles given as x, y, w, h overlap (touching edges do not count). */
|
|
174
|
+
export function rectsOverlap(
|
|
175
|
+
ax: number,
|
|
176
|
+
ay: number,
|
|
177
|
+
aw: number,
|
|
178
|
+
ah: number,
|
|
179
|
+
bx: number,
|
|
180
|
+
by: number,
|
|
181
|
+
bw: number,
|
|
182
|
+
bh: number,
|
|
183
|
+
): boolean {
|
|
184
|
+
return ax < bx + bw && bx < ax + aw && ay < by + bh && by < ay + ah;
|
|
185
|
+
}
|