@weasel-js/renderer 0.0.0-stage → 1.9.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/LICENSE +21 -0
- package/README.md +61 -2
- package/dist/chunk-2VOBNWBN.js +11 -0
- package/dist/chunk-2VOBNWBN.js.map +1 -0
- package/dist/chunk-ORBXHIRB.js +178 -0
- package/dist/chunk-ORBXHIRB.js.map +1 -0
- package/dist/chunk-VHQJ56XQ.js +1437 -0
- package/dist/chunk-VHQJ56XQ.js.map +1 -0
- package/dist/imageCache-oJVgxhkr.d.ts +372 -0
- package/dist/index.d.ts +1070 -0
- package/dist/index.js +4086 -0
- package/dist/index.js.map +1 -0
- package/dist/mesh.d.ts +274 -0
- package/dist/mesh.js +603 -0
- package/dist/mesh.js.map +1 -0
- package/dist/paintKinds-B2vEy9A4.d.ts +330 -0
- package/dist/patterns-builtin.d.ts +118 -0
- package/dist/patterns-builtin.js +28 -0
- package/dist/patterns-builtin.js.map +1 -0
- package/dist/test-seams.d.ts +120 -0
- package/dist/test-seams.js +4 -0
- package/dist/test-seams.js.map +1 -0
- package/dist/testing/glRecorder.d.ts +36 -0
- package/dist/testing/glRecorder.js +155 -0
- package/dist/testing/glRecorder.js.map +1 -0
- package/dist/testing/vertexUploads.d.ts +30 -0
- package/dist/testing/vertexUploads.js +48 -0
- package/dist/testing/vertexUploads.js.map +1 -0
- package/package.json +71 -4
|
@@ -0,0 +1,1437 @@
|
|
|
1
|
+
import { ownTextureIds, rasterizeTile, drawTileShapes, tileGeometry, tileSize, registerTextureAs, textureRegistry, unregisterTexture } from './chunk-ORBXHIRB.js';
|
|
2
|
+
import { tessellate } from '@weasel-js/geom/tessellate';
|
|
3
|
+
import { createReflectable } from '@weasel-js/registry';
|
|
4
|
+
import { resolveStrokeWidth, strokeInsets, tessellateStroke, markerGeneration, screenSpaceKey, markerHeads, metricKey, getMarker, markerKeyOf, markerReadsKey } from '@weasel-js/stroke';
|
|
5
|
+
import { pathFromD, boundsOfPath } from '@weasel-js/geom';
|
|
6
|
+
import { GLYPH_COVERAGE_GLSL } from '@weasel-js/font';
|
|
7
|
+
|
|
8
|
+
// src/cache/GLImageCache.ts
|
|
9
|
+
var DEFAULT_MAX_TEXTURE_SIDE = 4096;
|
|
10
|
+
var live = /* @__PURE__ */ new Set();
|
|
11
|
+
function releaseImageSource(key) {
|
|
12
|
+
for (const cache7 of live) cache7.release(key);
|
|
13
|
+
}
|
|
14
|
+
function maxImageTextureSide() {
|
|
15
|
+
let side = Infinity;
|
|
16
|
+
for (const cache7 of live) side = Math.min(side, cache7.maxTextureSide);
|
|
17
|
+
return Number.isFinite(side) ? side : DEFAULT_MAX_TEXTURE_SIDE;
|
|
18
|
+
}
|
|
19
|
+
var GLImageCache = class {
|
|
20
|
+
/** `minification` selects the MIN_FILTER strategy for uploaded textures.
|
|
21
|
+
* `'linear'` (default) is the screen path's existing behavior. `'mipmap'`
|
|
22
|
+
* generates mipmaps and filters LINEAR_MIPMAP_LINEAR — required for
|
|
23
|
+
* quality minification when a large source bitmap is drawn small (the
|
|
24
|
+
* headless print/export path); bilinear-only minification undersamples
|
|
25
|
+
* and produces moiré. */
|
|
26
|
+
constructor(gl, minification = "linear") {
|
|
27
|
+
this.gl = gl;
|
|
28
|
+
this.minification = minification;
|
|
29
|
+
const reported = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
30
|
+
this.maxTextureSide = typeof reported === "number" && reported > 0 ? reported : DEFAULT_MAX_TEXTURE_SIDE;
|
|
31
|
+
this.finalizer = new FinalizationRegistry((tex) => {
|
|
32
|
+
if (this.textures.delete(tex)) this.pendingDeletes.push(tex);
|
|
33
|
+
});
|
|
34
|
+
live.add(this);
|
|
35
|
+
}
|
|
36
|
+
gl;
|
|
37
|
+
minification;
|
|
38
|
+
/** This context's `MAX_TEXTURE_SIZE`. */
|
|
39
|
+
maxTextureSide;
|
|
40
|
+
map = /* @__PURE__ */ new WeakMap();
|
|
41
|
+
/** Every texture `map` holds, so `dispose` can reach them — a WeakMap
|
|
42
|
+
* cannot be enumerated. */
|
|
43
|
+
textures = /* @__PURE__ */ new Set();
|
|
44
|
+
/**
|
|
45
|
+
* MAG_FILTER each texture currently carries, so a redundant write can be
|
|
46
|
+
* skipped.
|
|
47
|
+
*
|
|
48
|
+
* The batch sets this per flush — the same bitmap can be drawn at both
|
|
49
|
+
* filters in one frame, and the value has to be right at the draw rather than
|
|
50
|
+
* at upload. Filtering is state on the *texture object*, though, not on the
|
|
51
|
+
* unit, so re-asserting a value it already has is a write to a live texture
|
|
52
|
+
* for no reason. On a large sheet that is not free: a consumer's wall samples
|
|
53
|
+
* a 5652px-square atlas, 122MB resident, and measured `sampling: 'nearest'`
|
|
54
|
+
* costing up to 8x `'linear'` there — where the linear pass re-asserts the
|
|
55
|
+
* upload default and the nearest pass changes state on every draw.
|
|
56
|
+
*
|
|
57
|
+
* Whether that is the cause is unproven (see `docs/TODO.md`), but the write
|
|
58
|
+
* was redundant either way.
|
|
59
|
+
*/
|
|
60
|
+
magFilters = /* @__PURE__ */ new WeakMap();
|
|
61
|
+
finalizer;
|
|
62
|
+
pendingDeletes = [];
|
|
63
|
+
/** Delete the textures of sources collected since the last call. The
|
|
64
|
+
* renderer calls this at the top of each `render()`. */
|
|
65
|
+
drainPendingDeletes() {
|
|
66
|
+
if (this.pendingDeletes.length === 0) return;
|
|
67
|
+
for (const tex of this.pendingDeletes) this.gl.deleteTexture(tex);
|
|
68
|
+
this.pendingDeletes.length = 0;
|
|
69
|
+
}
|
|
70
|
+
/** Delete `key`'s texture, if it has one. */
|
|
71
|
+
release(key) {
|
|
72
|
+
const tex = this.map.get(key);
|
|
73
|
+
if (!tex) return;
|
|
74
|
+
this.gl.deleteTexture(tex);
|
|
75
|
+
this.textures.delete(tex);
|
|
76
|
+
this.finalizer.unregister(key);
|
|
77
|
+
this.map.delete(key);
|
|
78
|
+
this.magFilters.delete(key);
|
|
79
|
+
}
|
|
80
|
+
/** Delete every texture this cache uploaded and stop answering
|
|
81
|
+
* {@link releaseImageSource} and {@link maxImageTextureSide}. Idempotent. */
|
|
82
|
+
dispose() {
|
|
83
|
+
for (const tex of this.textures) this.gl.deleteTexture(tex);
|
|
84
|
+
this.drainPendingDeletes();
|
|
85
|
+
this.abandon();
|
|
86
|
+
}
|
|
87
|
+
/** {@link dispose} without the deletes, for a cache whose context was lost:
|
|
88
|
+
* its textures died with it. */
|
|
89
|
+
abandon() {
|
|
90
|
+
this.textures.clear();
|
|
91
|
+
this.pendingDeletes.length = 0;
|
|
92
|
+
live.delete(this);
|
|
93
|
+
}
|
|
94
|
+
upload(key, source, repetition) {
|
|
95
|
+
const existing = this.map.get(key);
|
|
96
|
+
if (existing) return existing;
|
|
97
|
+
const gl = this.gl;
|
|
98
|
+
const tex = gl.createTexture();
|
|
99
|
+
if (!tex) throw new Error("GLImageCache: createTexture failed");
|
|
100
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
101
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
|
|
102
|
+
gl.texParameteri(
|
|
103
|
+
gl.TEXTURE_2D,
|
|
104
|
+
gl.TEXTURE_MIN_FILTER,
|
|
105
|
+
this.minification === "mipmap" ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR
|
|
106
|
+
);
|
|
107
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
108
|
+
this.magFilters.set(key, gl.LINEAR);
|
|
109
|
+
const [wrapS, wrapT] = wrapModes(gl, repetition);
|
|
110
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);
|
|
111
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);
|
|
112
|
+
if (this.minification === "mipmap") gl.generateMipmap(gl.TEXTURE_2D);
|
|
113
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
114
|
+
this.map.set(key, tex);
|
|
115
|
+
this.textures.add(tex);
|
|
116
|
+
this.finalizer.register(key, tex, key);
|
|
117
|
+
return tex;
|
|
118
|
+
}
|
|
119
|
+
bind(key, unit) {
|
|
120
|
+
const tex = this.map.get(key);
|
|
121
|
+
if (!tex) throw new Error("GLImageCache: image not uploaded; call upload() first");
|
|
122
|
+
const gl = this.gl;
|
|
123
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
124
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
125
|
+
}
|
|
126
|
+
/** Set `key`'s MAG_FILTER, skipping the call when it already holds that
|
|
127
|
+
* value. Its texture must be bound to the active unit — callers pair this
|
|
128
|
+
* with `bind`. */
|
|
129
|
+
setMagFilter(key, filter) {
|
|
130
|
+
if (this.magFilters.get(key) === filter) return;
|
|
131
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, filter);
|
|
132
|
+
this.magFilters.set(key, filter);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
function wrapModes(gl, rep) {
|
|
136
|
+
switch (rep) {
|
|
137
|
+
case "repeat":
|
|
138
|
+
return [gl.REPEAT, gl.REPEAT];
|
|
139
|
+
case "repeat-x":
|
|
140
|
+
return [gl.REPEAT, gl.CLAMP_TO_EDGE];
|
|
141
|
+
case "repeat-y":
|
|
142
|
+
return [gl.CLAMP_TO_EDGE, gl.REPEAT];
|
|
143
|
+
default:
|
|
144
|
+
return [gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
148
|
+
function getMesh(path) {
|
|
149
|
+
const cached = cache.get(path);
|
|
150
|
+
if (cached !== void 0) return cached;
|
|
151
|
+
const mesh = tessellate(path);
|
|
152
|
+
cache.set(path, mesh);
|
|
153
|
+
return mesh;
|
|
154
|
+
}
|
|
155
|
+
function _resetCacheForTests() {
|
|
156
|
+
cache = /* @__PURE__ */ new WeakMap();
|
|
157
|
+
}
|
|
158
|
+
var registry = createReflectable();
|
|
159
|
+
var programSourceRegistry = registry.reflection;
|
|
160
|
+
function _resetProgramRegistryForTests() {
|
|
161
|
+
registry.clear();
|
|
162
|
+
}
|
|
163
|
+
function getProgramSource(id) {
|
|
164
|
+
return registry.get(id) ?? null;
|
|
165
|
+
}
|
|
166
|
+
var isDev = () => typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
|
|
167
|
+
function registerProgram(id, vert, frag) {
|
|
168
|
+
if (registry.has(id) && !isDev()) {
|
|
169
|
+
throw new Error(`weasel registerProgram: duplicate program id "${id}". In production, re-registration is not allowed. Pass a unique id or call in dev mode.`);
|
|
170
|
+
}
|
|
171
|
+
registry.set(id, { vert, frag });
|
|
172
|
+
return { id };
|
|
173
|
+
}
|
|
174
|
+
var KINDS = createReflectable();
|
|
175
|
+
var paintDrawKindRegistry = KINDS.reflection;
|
|
176
|
+
function registerPaintDrawKind(entry) {
|
|
177
|
+
if (entry.inPoseFrame === void 0 !== (entry.toBoundsFrame === void 0)) {
|
|
178
|
+
const missing = entry.inPoseFrame === void 0 ? "inPoseFrame" : "toBoundsFrame";
|
|
179
|
+
throw new Error(
|
|
180
|
+
`weasel registerPaintKind: kind "${entry.id}" is missing ${missing}. A kind that converts one frame direction and not the other drifts on the next resize; supply both or neither.`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
for (const [id, src] of Object.entries(entry.programs ?? {})) {
|
|
184
|
+
const have = getProgramSource(id);
|
|
185
|
+
if (!have || have.vert !== src.vert || have.frag !== src.frag) registerProgram(id, src.vert, src.frag);
|
|
186
|
+
}
|
|
187
|
+
return KINDS.push(entry.id, entry);
|
|
188
|
+
}
|
|
189
|
+
function getPaintDrawKind(kind) {
|
|
190
|
+
const id = kind ?? "solid";
|
|
191
|
+
const entry = KINDS.get(id);
|
|
192
|
+
if (!entry && LOADERS.has(id)) void startLoad(id).catch(() => {
|
|
193
|
+
});
|
|
194
|
+
return entry;
|
|
195
|
+
}
|
|
196
|
+
var LOADERS = /* @__PURE__ */ new Map();
|
|
197
|
+
var LOADS = /* @__PURE__ */ new Map();
|
|
198
|
+
function registerPaintDrawKindLoader(id, load) {
|
|
199
|
+
LOADERS.set(id, load);
|
|
200
|
+
return () => {
|
|
201
|
+
if (LOADERS.get(id) === load) LOADERS.delete(id);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function startLoad(id) {
|
|
205
|
+
const running = LOADS.get(id);
|
|
206
|
+
if (running) return running;
|
|
207
|
+
const load = LOADERS.get(id);
|
|
208
|
+
const done = load().then(
|
|
209
|
+
(entry) => {
|
|
210
|
+
if (entry.id !== id) {
|
|
211
|
+
throw new Error(`weasel: the loader for paint kind "${id}" resolved to "${entry.id}".`);
|
|
212
|
+
}
|
|
213
|
+
if (!KINDS.has(id)) registerPaintDrawKind(entry);
|
|
214
|
+
}
|
|
215
|
+
).catch((err) => {
|
|
216
|
+
console.error(`weasel: paint kind "${id}" failed to load:`, err);
|
|
217
|
+
throw err;
|
|
218
|
+
});
|
|
219
|
+
LOADS.set(id, done);
|
|
220
|
+
return done;
|
|
221
|
+
}
|
|
222
|
+
function isPaintKindKnown(id) {
|
|
223
|
+
return KINDS.has(id) || LOADERS.has(id);
|
|
224
|
+
}
|
|
225
|
+
function warmPaintKinds(kinds) {
|
|
226
|
+
const ids = kinds ?? [...LOADERS.keys()];
|
|
227
|
+
const loads = [];
|
|
228
|
+
for (const id of ids) {
|
|
229
|
+
if (KINDS.has(id)) continue;
|
|
230
|
+
if (!LOADERS.has(id)) {
|
|
231
|
+
return Promise.reject(new Error(
|
|
232
|
+
`weasel warmPaintKinds: "${id}" is neither registered nor loadable.`
|
|
233
|
+
));
|
|
234
|
+
}
|
|
235
|
+
loads.push(startLoad(id));
|
|
236
|
+
}
|
|
237
|
+
return Promise.all(loads).then(() => void 0);
|
|
238
|
+
}
|
|
239
|
+
function _resetPaintDrawKindsForTests() {
|
|
240
|
+
KINDS.clear();
|
|
241
|
+
LOADERS.clear();
|
|
242
|
+
LOADS.clear();
|
|
243
|
+
}
|
|
244
|
+
var STROKE_CONFIGS_PER_PATH = 8;
|
|
245
|
+
var PX_WIDTH_TOLERANCE = 1 / 8;
|
|
246
|
+
function quantizeStrokeScale(px, scale) {
|
|
247
|
+
if (!(px > 0) || !(scale > 0) || !Number.isFinite(scale)) return scale;
|
|
248
|
+
const stepsPerOctave = Math.ceil(Math.LN2 / (2 * Math.log1p(PX_WIDTH_TOLERANCE / px)));
|
|
249
|
+
return 2 ** (Math.round(Math.log2(scale) * stepsPerOctave) / stepsPerOctave);
|
|
250
|
+
}
|
|
251
|
+
var cache2 = /* @__PURE__ */ new WeakMap();
|
|
252
|
+
function configKey(stroke, flattenTolerance, spaces) {
|
|
253
|
+
const width = resolveStrokeWidth(stroke.width ?? 1, 1);
|
|
254
|
+
const insets = strokeInsets(stroke, width);
|
|
255
|
+
const ribbon = spaces.ribbon?.metric;
|
|
256
|
+
return [
|
|
257
|
+
width,
|
|
258
|
+
stroke.cap ?? "butt",
|
|
259
|
+
stroke.join ?? "miter",
|
|
260
|
+
stroke.miterLimit ?? "",
|
|
261
|
+
stroke.align ?? "center",
|
|
262
|
+
(stroke.dash ?? []).join(","),
|
|
263
|
+
stroke.varyingWidthJoinThreshold ?? "",
|
|
264
|
+
flattenTolerance ?? "",
|
|
265
|
+
insets.start,
|
|
266
|
+
insets.end,
|
|
267
|
+
metricKey(ribbon),
|
|
268
|
+
metricKey(spaces.start?.metric),
|
|
269
|
+
metricKey(spaces.end?.metric)
|
|
270
|
+
].join("|");
|
|
271
|
+
}
|
|
272
|
+
function strokeMesh(path, stroke, flattenTolerance, spaces = {}) {
|
|
273
|
+
let byConfig = cache2.get(path);
|
|
274
|
+
if (byConfig === void 0) {
|
|
275
|
+
byConfig = /* @__PURE__ */ new Map();
|
|
276
|
+
cache2.set(path, byConfig);
|
|
277
|
+
}
|
|
278
|
+
const key = configKey(stroke, flattenTolerance, spaces);
|
|
279
|
+
const entry = byConfig.get(key);
|
|
280
|
+
if (entry !== void 0 && entry.vertexWidths === stroke.vertexWidths) {
|
|
281
|
+
return entry.mesh;
|
|
282
|
+
}
|
|
283
|
+
const width = resolveStrokeWidth(stroke.width ?? 1, 1);
|
|
284
|
+
const insets = strokeInsets(stroke, width);
|
|
285
|
+
const mesh = tessellateStroke(path, stroke, {
|
|
286
|
+
flattenTolerance,
|
|
287
|
+
startInset: insetIn(insets.start, spaces.start, spaces.ribbon),
|
|
288
|
+
endInset: insetIn(insets.end, spaces.end, spaces.ribbon),
|
|
289
|
+
metric: spaces.ribbon?.metric
|
|
290
|
+
});
|
|
291
|
+
if (entry === void 0 && byConfig.size >= STROKE_CONFIGS_PER_PATH) byConfig.clear();
|
|
292
|
+
byConfig.set(key, { mesh, vertexWidths: stroke.vertexWidths });
|
|
293
|
+
return mesh;
|
|
294
|
+
}
|
|
295
|
+
function insetIn(length, head, ribbon) {
|
|
296
|
+
return head?.metric === ribbon?.metric ? length : { length, metric: head?.metric ?? null };
|
|
297
|
+
}
|
|
298
|
+
function _resetStrokeMeshCacheForTests() {
|
|
299
|
+
cache2 = /* @__PURE__ */ new WeakMap();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/fillInPoseFrame.ts
|
|
303
|
+
function fillInPoseFrame(fill, box) {
|
|
304
|
+
if (!isBuiltinKind(fill)) {
|
|
305
|
+
return getPaintDrawKind(fill.fill)?.inPoseFrame?.(fill, box) ?? fill;
|
|
306
|
+
}
|
|
307
|
+
if (isBoundsPattern(fill)) {
|
|
308
|
+
const o = fill.origin ?? { x: 0, y: 0 };
|
|
309
|
+
return { ...fill, origin: { x: box.x + o.x, y: box.y + o.y }, units: "local" };
|
|
310
|
+
}
|
|
311
|
+
if (!isBoundsGradient(fill)) return fill;
|
|
312
|
+
const toBox = (p) => ({
|
|
313
|
+
x: box.x + p.x * box.width,
|
|
314
|
+
y: box.y + p.y * box.height
|
|
315
|
+
});
|
|
316
|
+
if (fill.fill === "linear-gradient") {
|
|
317
|
+
return { ...fill, from: toBox(fill.from), to: toBox(fill.to), units: "local" };
|
|
318
|
+
}
|
|
319
|
+
if (fill.fill === "radial-gradient") {
|
|
320
|
+
const scale = Math.hypot(box.width, box.height) / Math.SQRT2;
|
|
321
|
+
return { ...fill, center: toBox(fill.center), radius: fill.radius * scale, units: "local" };
|
|
322
|
+
}
|
|
323
|
+
return { ...fill, center: toBox(fill.center), units: "local" };
|
|
324
|
+
}
|
|
325
|
+
function fillToBoundsFrame(fill, box) {
|
|
326
|
+
if (!isBuiltinKind(fill)) {
|
|
327
|
+
return getPaintDrawKind(fill.fill)?.toBoundsFrame?.(fill, box) ?? fill;
|
|
328
|
+
}
|
|
329
|
+
if (fill.fill === "pattern") {
|
|
330
|
+
const o = fill.origin ?? { x: 0, y: 0 };
|
|
331
|
+
return { ...fill, origin: { x: o.x - box.x, y: o.y - box.y }, units: "bounds" };
|
|
332
|
+
}
|
|
333
|
+
const isGradient = fill.fill === "linear-gradient" || fill.fill === "radial-gradient" || fill.fill === "conic-gradient";
|
|
334
|
+
if (!isGradient) return fill;
|
|
335
|
+
if (box.width === 0 || box.height === 0) return fill;
|
|
336
|
+
const fromBox = (p) => ({
|
|
337
|
+
x: (p.x - box.x) / box.width,
|
|
338
|
+
y: (p.y - box.y) / box.height
|
|
339
|
+
});
|
|
340
|
+
if (fill.fill === "linear-gradient") {
|
|
341
|
+
return { ...fill, from: fromBox(fill.from), to: fromBox(fill.to), units: "bounds" };
|
|
342
|
+
}
|
|
343
|
+
if (fill.fill === "radial-gradient") {
|
|
344
|
+
const scale = Math.hypot(box.width, box.height) / Math.SQRT2;
|
|
345
|
+
return { ...fill, center: fromBox(fill.center), radius: fill.radius / scale, units: "bounds" };
|
|
346
|
+
}
|
|
347
|
+
return { ...fill, center: fromBox(fill.center), units: "bounds" };
|
|
348
|
+
}
|
|
349
|
+
function isBoundsPattern(fill) {
|
|
350
|
+
return fill.fill === "pattern" && fill.units === "bounds";
|
|
351
|
+
}
|
|
352
|
+
function isBoundsGradient(fill) {
|
|
353
|
+
if (fill === void 0) return false;
|
|
354
|
+
const kind = fill.fill;
|
|
355
|
+
if (kind !== "linear-gradient" && kind !== "radial-gradient" && kind !== "conic-gradient") {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
return fill.units === "bounds";
|
|
359
|
+
}
|
|
360
|
+
function isBuiltinKind(fill) {
|
|
361
|
+
const kind = fill.fill ?? "solid";
|
|
362
|
+
return kind === "solid" || kind === "pattern" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "conic-gradient";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/patterns/resolveSpec.ts
|
|
366
|
+
var PATTERN_TILE_CACHE_LIMIT = 256;
|
|
367
|
+
var ID_PREFIX = "pattern:";
|
|
368
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
369
|
+
function specId(spec) {
|
|
370
|
+
const bag = spec;
|
|
371
|
+
const sorted = {};
|
|
372
|
+
for (const k of Object.keys(bag).sort()) sorted[k] = bag[k];
|
|
373
|
+
return ID_PREFIX + JSON.stringify(sorted);
|
|
374
|
+
}
|
|
375
|
+
function _resetPatternSpecCacheForTests() {
|
|
376
|
+
cache3.clear();
|
|
377
|
+
}
|
|
378
|
+
function resolvePatternSpec(spec) {
|
|
379
|
+
const id = specId(spec);
|
|
380
|
+
const hit = cache3.get(id);
|
|
381
|
+
if (hit !== void 0) {
|
|
382
|
+
markUsed(id);
|
|
383
|
+
return hit;
|
|
384
|
+
}
|
|
385
|
+
return build(id, spec);
|
|
386
|
+
}
|
|
387
|
+
function markUsed(id) {
|
|
388
|
+
const hit = cache3.get(id);
|
|
389
|
+
if (hit === void 0) return;
|
|
390
|
+
cache3.delete(id);
|
|
391
|
+
cache3.set(id, hit);
|
|
392
|
+
}
|
|
393
|
+
function build(id, spec) {
|
|
394
|
+
const bitmap = TILES.has(spec.tile) ? rasterizeTile({
|
|
395
|
+
size: tileSize(spec),
|
|
396
|
+
draw: (oc) => drawTileShapes(oc, tileGeometry(spec))
|
|
397
|
+
}) : null;
|
|
398
|
+
const handle = bitmap ? registerTextureAs(id, bitmap) : null;
|
|
399
|
+
cache3.delete(id);
|
|
400
|
+
cache3.set(id, handle);
|
|
401
|
+
while (cache3.size > PATTERN_TILE_CACHE_LIMIT) evict(cache3.keys().next().value);
|
|
402
|
+
return handle;
|
|
403
|
+
}
|
|
404
|
+
var TILES = /* @__PURE__ */ new Set([
|
|
405
|
+
"hatch",
|
|
406
|
+
"crosshatch",
|
|
407
|
+
"dots",
|
|
408
|
+
"chunks"
|
|
409
|
+
]);
|
|
410
|
+
function evict(id) {
|
|
411
|
+
cache3.delete(id);
|
|
412
|
+
const source = textureRegistry.get(id)?.source;
|
|
413
|
+
unregisterTexture({ id });
|
|
414
|
+
if (source && "close" in source) source.close();
|
|
415
|
+
}
|
|
416
|
+
function revive(id) {
|
|
417
|
+
let spec;
|
|
418
|
+
try {
|
|
419
|
+
spec = JSON.parse(id.slice(ID_PREFIX.length));
|
|
420
|
+
} catch {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (typeof spec !== "object" || spec === null || !("tile" in spec)) return;
|
|
424
|
+
build(id, spec);
|
|
425
|
+
}
|
|
426
|
+
ownTextureIds(ID_PREFIX, { revive, touched: markUsed });
|
|
427
|
+
function isPatternSpec(payload) {
|
|
428
|
+
return "tile" in payload;
|
|
429
|
+
}
|
|
430
|
+
function resolveFillPattern(fill) {
|
|
431
|
+
if (fill.fill !== "pattern") return fill;
|
|
432
|
+
if (!isPatternSpec(fill.pattern)) return fill;
|
|
433
|
+
const handle = resolvePatternSpec(fill.pattern);
|
|
434
|
+
return handle ? { ...fill, pattern: handle } : null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/patterns/commandPaint.ts
|
|
438
|
+
function commandFill(fill, box) {
|
|
439
|
+
return resolveFillPattern(fillInPoseFrame(fill, box));
|
|
440
|
+
}
|
|
441
|
+
function commandStroke(stroke, box) {
|
|
442
|
+
if (stroke.paint == null) return null;
|
|
443
|
+
const paint = commandFill(stroke.paint, box);
|
|
444
|
+
if (paint === null) return null;
|
|
445
|
+
return paint === stroke.paint ? stroke : { ...stroke, paint };
|
|
446
|
+
}
|
|
447
|
+
function resolvePaint(p, stroke, fallback, head) {
|
|
448
|
+
const v = p ?? fallback;
|
|
449
|
+
if (v === "none") return void 0;
|
|
450
|
+
if (v === "line") return stroke.paint;
|
|
451
|
+
return commandFill(v, boundsOfPath(head)) ?? void 0;
|
|
452
|
+
}
|
|
453
|
+
function headCommands(heads, stroke) {
|
|
454
|
+
const out = [];
|
|
455
|
+
for (const { entry, path, size, space } of heads) {
|
|
456
|
+
const fill = resolvePaint(entry.fill, stroke, "line", path);
|
|
457
|
+
const width = entry.outline ? entry.outline.width * size : 0;
|
|
458
|
+
const outline = entry.outline ? { paint: resolvePaint(entry.outline.paint, stroke, "line", path), width: space ? { px: width * space.pxPerUnit } : width } : null;
|
|
459
|
+
if (fill === void 0 && (outline === null || outline.paint === void 0)) continue;
|
|
460
|
+
out.push({
|
|
461
|
+
kind: "path",
|
|
462
|
+
path,
|
|
463
|
+
...fill ? { fill } : {},
|
|
464
|
+
...outline && outline.paint ? { stroke: { paint: outline.paint, width: outline.width, cap: "round", join: "round" } } : {}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
var byValue = /* @__PURE__ */ new WeakMap();
|
|
470
|
+
var byStroke = /* @__PURE__ */ new WeakMap();
|
|
471
|
+
var refKey = (ref) => ref === void 0 ? "" : typeof ref === "string" ? ref : `${ref.key}@${String(ref.size ?? "")}`;
|
|
472
|
+
function readsKey(stroke) {
|
|
473
|
+
let key = "";
|
|
474
|
+
for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
|
|
475
|
+
const entry = ref === void 0 ? void 0 : getMarker(markerKeyOf(ref));
|
|
476
|
+
if (entry === void 0) {
|
|
477
|
+
key += "/";
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const reads = markerReadsKey(entry, stroke);
|
|
481
|
+
if (reads === null) return null;
|
|
482
|
+
key += `${reads}/`;
|
|
483
|
+
}
|
|
484
|
+
return key;
|
|
485
|
+
}
|
|
486
|
+
function mapFor(path, source, reads) {
|
|
487
|
+
if (reads !== null) {
|
|
488
|
+
let map2 = byValue.get(path);
|
|
489
|
+
if (map2 === void 0) byValue.set(path, map2 = /* @__PURE__ */ new Map());
|
|
490
|
+
return map2;
|
|
491
|
+
}
|
|
492
|
+
let strokes = byStroke.get(path);
|
|
493
|
+
if (strokes === void 0) byStroke.set(path, strokes = /* @__PURE__ */ new WeakMap());
|
|
494
|
+
let map = strokes.get(source);
|
|
495
|
+
if (map === void 0) strokes.set(source, map = /* @__PURE__ */ new Map());
|
|
496
|
+
return map;
|
|
497
|
+
}
|
|
498
|
+
function cachedMarkerCommands(path, source, stroke, strokeWidth, flattenTolerance, spaces = {}) {
|
|
499
|
+
if (stroke.markerStart === void 0 && stroke.markerMid === void 0 && stroke.markerEnd === void 0) {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
const reads = readsKey(stroke);
|
|
503
|
+
const map = mapFor(path, source, reads);
|
|
504
|
+
const key = [
|
|
505
|
+
strokeWidth,
|
|
506
|
+
flattenTolerance ?? "",
|
|
507
|
+
markerGeneration(),
|
|
508
|
+
refKey(stroke.markerStart),
|
|
509
|
+
refKey(stroke.markerMid),
|
|
510
|
+
refKey(stroke.markerEnd),
|
|
511
|
+
reads ?? "",
|
|
512
|
+
screenSpaceKey(spaces.start),
|
|
513
|
+
screenSpaceKey(spaces.mid),
|
|
514
|
+
screenSpaceKey(spaces.end)
|
|
515
|
+
].join("|");
|
|
516
|
+
let entry = map.get(key);
|
|
517
|
+
if (entry === void 0) {
|
|
518
|
+
const heads = markerHeads(path, stroke, strokeWidth, flattenTolerance, spaces);
|
|
519
|
+
entry = { heads, paint: stroke.paint, cmds: headCommands(heads, stroke) };
|
|
520
|
+
if (map.size >= STROKE_CONFIGS_PER_PATH) map.clear();
|
|
521
|
+
map.set(key, entry);
|
|
522
|
+
} else if (entry.paint !== stroke.paint) {
|
|
523
|
+
entry.paint = stroke.paint;
|
|
524
|
+
entry.cmds = headCommands(entry.heads, stroke);
|
|
525
|
+
}
|
|
526
|
+
return entry.cmds;
|
|
527
|
+
}
|
|
528
|
+
function _resetMarkerCommandCacheForTests() {
|
|
529
|
+
byValue = /* @__PURE__ */ new WeakMap();
|
|
530
|
+
byStroke = /* @__PURE__ */ new WeakMap();
|
|
531
|
+
}
|
|
532
|
+
var OUTLINE_FLATTEN_TOLERANCE = 1 / 4096;
|
|
533
|
+
var OUTLINE_MESH_CACHE_LIMIT = 2048;
|
|
534
|
+
var cache4 = /* @__PURE__ */ new Map();
|
|
535
|
+
function outlineMesh(key, d) {
|
|
536
|
+
const cached = cache4.get(key);
|
|
537
|
+
if (cached !== void 0) return cached;
|
|
538
|
+
if (cache4.size >= OUTLINE_MESH_CACHE_LIMIT) cache4 = /* @__PURE__ */ new Map();
|
|
539
|
+
const mesh = tessellate(pathFromD(d), { flattenTolerance: OUTLINE_FLATTEN_TOLERANCE });
|
|
540
|
+
cache4.set(key, mesh);
|
|
541
|
+
return mesh;
|
|
542
|
+
}
|
|
543
|
+
function _resetOutlineMeshCacheForTests() {
|
|
544
|
+
cache4 = /* @__PURE__ */ new Map();
|
|
545
|
+
}
|
|
546
|
+
var OUTLINE_STROKE_MESH_CACHE_LIMIT = 2048;
|
|
547
|
+
var WIDTH_QUANTUM = 1 / 1024;
|
|
548
|
+
var cache5 = /* @__PURE__ */ new Map();
|
|
549
|
+
function quantizeEmWidth(emWidth) {
|
|
550
|
+
return Math.round(emWidth / WIDTH_QUANTUM) * WIDTH_QUANTUM;
|
|
551
|
+
}
|
|
552
|
+
function outlineStrokeMesh(glyphKey, d, emWidth, stroke, metric) {
|
|
553
|
+
if (!(emWidth > 0)) return null;
|
|
554
|
+
const key = [
|
|
555
|
+
glyphKey,
|
|
556
|
+
emWidth,
|
|
557
|
+
stroke.join ?? "miter",
|
|
558
|
+
stroke.cap ?? "butt",
|
|
559
|
+
stroke.miterLimit ?? "",
|
|
560
|
+
stroke.align ?? "center",
|
|
561
|
+
(stroke.dash ?? []).join(","),
|
|
562
|
+
metricKey(metric)
|
|
563
|
+
].join("|");
|
|
564
|
+
const cached = cache5.get(key);
|
|
565
|
+
if (cached !== void 0) return cached.indices.length === 0 ? null : cached;
|
|
566
|
+
if (cache5.size >= OUTLINE_STROKE_MESH_CACHE_LIMIT) cache5 = /* @__PURE__ */ new Map();
|
|
567
|
+
const mesh = tessellateStroke(
|
|
568
|
+
pathFromD(d),
|
|
569
|
+
{ ...stroke, width: emWidth },
|
|
570
|
+
{ flattenTolerance: OUTLINE_FLATTEN_TOLERANCE, metric }
|
|
571
|
+
);
|
|
572
|
+
cache5.set(key, mesh);
|
|
573
|
+
return mesh.indices.length === 0 ? null : mesh;
|
|
574
|
+
}
|
|
575
|
+
function _resetOutlineStrokeMeshCacheForTests() {
|
|
576
|
+
cache5 = /* @__PURE__ */ new Map();
|
|
577
|
+
}
|
|
578
|
+
var BATCH_TEXTURE_SLOTS = 8;
|
|
579
|
+
var WHITE_SLOT = 0;
|
|
580
|
+
var PAINT_MODE_PLAIN = 0;
|
|
581
|
+
var PAINT_MODE_RADIAL = 3;
|
|
582
|
+
var PAINT_MODE_CONIC = 4;
|
|
583
|
+
var INV_TAU = (1 / (2 * Math.PI)).toFixed(10);
|
|
584
|
+
function packSlot(slot, mode) {
|
|
585
|
+
return slot + BATCH_TEXTURE_SLOTS * mode;
|
|
586
|
+
}
|
|
587
|
+
var BATCH_ATTRIBUTE_LOCATIONS = {
|
|
588
|
+
a_position: 0,
|
|
589
|
+
a_vertexColor: 1,
|
|
590
|
+
a_uv: 2,
|
|
591
|
+
a_post: 3,
|
|
592
|
+
a_texSlot: 4
|
|
593
|
+
};
|
|
594
|
+
var L = BATCH_ATTRIBUTE_LOCATIONS;
|
|
595
|
+
function sampleChain(slots) {
|
|
596
|
+
const arms = [];
|
|
597
|
+
for (let i = 1; i < slots; i++) {
|
|
598
|
+
arms.push(` if (slot == ${i}) return texture(u_samplers[${i}], uv);`);
|
|
599
|
+
}
|
|
600
|
+
return arms.join("\n");
|
|
601
|
+
}
|
|
602
|
+
var BATCH_VERT_SRC = (
|
|
603
|
+
/* glsl */
|
|
604
|
+
`#version 300 es
|
|
605
|
+
layout(location = ${L.a_position}) in vec2 a_position;
|
|
606
|
+
layout(location = ${L.a_vertexColor}) in vec4 a_vertexColor;
|
|
607
|
+
layout(location = ${L.a_uv}) in vec2 a_uv;
|
|
608
|
+
layout(location = ${L.a_post}) in float a_post;
|
|
609
|
+
layout(location = ${L.a_texSlot}) in float a_texSlot;
|
|
610
|
+
uniform mat3 u_proj;
|
|
611
|
+
uniform mat3 u_model;
|
|
612
|
+
out vec4 v_vertexColor;
|
|
613
|
+
out vec2 v_uv;
|
|
614
|
+
out float v_post;
|
|
615
|
+
flat out int v_texSlot;
|
|
616
|
+
flat out float v_paintMode;
|
|
617
|
+
void main() {
|
|
618
|
+
vec3 screen = u_model * vec3(a_position, 1.0);
|
|
619
|
+
vec3 clip = u_proj * vec3(screen.xy, 1.0);
|
|
620
|
+
gl_Position = vec4(clip.xy, 0.0, 1.0);
|
|
621
|
+
v_vertexColor = a_vertexColor;
|
|
622
|
+
v_uv = a_uv;
|
|
623
|
+
v_post = a_post;
|
|
624
|
+
// slot + 8 * mode, unpacked \u2014 see the file header for why they share a float.
|
|
625
|
+
int packed = int(a_texSlot + 0.5);
|
|
626
|
+
v_texSlot = packed % ${BATCH_TEXTURE_SLOTS};
|
|
627
|
+
v_paintMode = float(packed / ${BATCH_TEXTURE_SLOTS});
|
|
628
|
+
}
|
|
629
|
+
`
|
|
630
|
+
);
|
|
631
|
+
var BATCH_FRAG_SRC = (
|
|
632
|
+
/* glsl */
|
|
633
|
+
`#version 300 es
|
|
634
|
+
precision highp float;
|
|
635
|
+
in vec4 v_vertexColor;
|
|
636
|
+
in vec2 v_uv;
|
|
637
|
+
in float v_post;
|
|
638
|
+
flat in int v_texSlot;
|
|
639
|
+
flat in float v_paintMode;
|
|
640
|
+
uniform sampler2D u_samplers[${BATCH_TEXTURE_SLOTS}];
|
|
641
|
+
uniform vec4 u_color;
|
|
642
|
+
uniform float u_alpha;
|
|
643
|
+
uniform float u_synthBold;
|
|
644
|
+
uniform vec2 u_fieldScale[${BATCH_TEXTURE_SLOTS}];
|
|
645
|
+
uniform mat4 u_colorMatrix;
|
|
646
|
+
uniform vec4 u_colorBias;
|
|
647
|
+
out vec4 outColor;
|
|
648
|
+
|
|
649
|
+
vec4 sampleSlot(int slot, vec2 uv) {
|
|
650
|
+
${sampleChain(BATCH_TEXTURE_SLOTS)}
|
|
651
|
+
return texture(u_samplers[0], uv);
|
|
652
|
+
}
|
|
653
|
+
${GLYPH_COVERAGE_GLSL}
|
|
654
|
+
void main() {
|
|
655
|
+
float isGrad = step(2.5, v_paintMode);
|
|
656
|
+
// A radial or conic vertex carries a gradient-space coordinate rather than a
|
|
657
|
+
// texture coordinate, and its atlas row rather than an alpha. The sample is
|
|
658
|
+
// split across the branch rather than the coordinate selected before it: an
|
|
659
|
+
// arm that reads v_uv straight is a fetch the hardware can schedule against a
|
|
660
|
+
// varying, and one reading a coordinate the shader computed is not. Selecting
|
|
661
|
+
// the coordinate first and sampling once costs 29-54% of a fragment that is
|
|
662
|
+
// not a gradient; splitting it is within the measurement's own spread of free
|
|
663
|
+
// (tests/perf/fill-rate.spec.ts, teitou, 2026-10-05).
|
|
664
|
+
//
|
|
665
|
+
// Branching at all is safe here where it would not be around the glyph math:
|
|
666
|
+
// v_paintMode is flat, so every fragment of a quad takes the same arm, and
|
|
667
|
+
// nothing in either arm is a derivative.
|
|
668
|
+
vec4 texel;
|
|
669
|
+
if (isGrad > 0.5) {
|
|
670
|
+
float t = v_paintMode > ${PAINT_MODE_CONIC - 0.5}
|
|
671
|
+
? fract(atan(v_uv.y, v_uv.x) * ${INV_TAU})
|
|
672
|
+
: length(v_uv);
|
|
673
|
+
texel = sampleSlot(v_texSlot, vec2(t, v_post));
|
|
674
|
+
} else {
|
|
675
|
+
texel = sampleSlot(v_texSlot, v_uv);
|
|
676
|
+
}
|
|
677
|
+
float isGlyph = step(0.5, v_paintMode) * step(v_paintMode, 2.5);
|
|
678
|
+
// Unconditional, and multiplied out afterwards rather than branched around:
|
|
679
|
+
// see the file header.
|
|
680
|
+
float coverage = glyphCoverage(texel, v_paintMode, u_synthBold, v_uv, u_fieldScale[v_texSlot]);
|
|
681
|
+
// A glyph's texel is a distance field, so it takes no part in the color; a
|
|
682
|
+
// plain vertex multiplies its texel in as it always did.
|
|
683
|
+
vec4 src = mix(texel, vec4(1.0), isGlyph) * u_color * v_vertexColor;
|
|
684
|
+
vec4 mapped = clamp(u_colorMatrix * src + u_colorBias, 0.0, 1.0);
|
|
685
|
+
float a = mapped.a * u_alpha * mix(v_post, 1.0, isGrad) * mix(1.0, coverage, isGlyph);
|
|
686
|
+
outColor = vec4(mapped.rgb * a, a);
|
|
687
|
+
}
|
|
688
|
+
`
|
|
689
|
+
);
|
|
690
|
+
var BATCH_FILL_UNIFORMS = [
|
|
691
|
+
"u_proj",
|
|
692
|
+
"u_model",
|
|
693
|
+
"u_samplers",
|
|
694
|
+
"u_color",
|
|
695
|
+
"u_alpha",
|
|
696
|
+
"u_synthBold",
|
|
697
|
+
"u_fieldScale",
|
|
698
|
+
"u_colorMatrix",
|
|
699
|
+
"u_colorBias"
|
|
700
|
+
];
|
|
701
|
+
var BATCH_FILL_ATTRIBUTES = [
|
|
702
|
+
"a_position",
|
|
703
|
+
"a_vertexColor",
|
|
704
|
+
"a_uv",
|
|
705
|
+
"a_post",
|
|
706
|
+
"a_texSlot"
|
|
707
|
+
];
|
|
708
|
+
|
|
709
|
+
// src/batchClip.ts
|
|
710
|
+
function axisAlignedClipRect(path, m) {
|
|
711
|
+
if (path.kind !== "rect") return null;
|
|
712
|
+
const keepsAxes = m[1] === 0 && m[3] === 0 || m[0] === 0 && m[4] === 0;
|
|
713
|
+
if (!keepsAxes) return null;
|
|
714
|
+
const ax = m[0] * path.x + m[3] * path.y + m[6];
|
|
715
|
+
const ay = m[1] * path.x + m[4] * path.y + m[7];
|
|
716
|
+
const x1 = path.x + path.width;
|
|
717
|
+
const y1 = path.y + path.height;
|
|
718
|
+
const bx = m[0] * x1 + m[3] * y1 + m[6];
|
|
719
|
+
const by = m[1] * x1 + m[4] * y1 + m[7];
|
|
720
|
+
return {
|
|
721
|
+
x0: Math.min(ax, bx),
|
|
722
|
+
y0: Math.min(ay, by),
|
|
723
|
+
x1: Math.max(ax, bx),
|
|
724
|
+
y1: Math.max(ay, by)
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function intersectClipRects(a, b) {
|
|
728
|
+
return {
|
|
729
|
+
x0: Math.max(a.x0, b.x0),
|
|
730
|
+
y0: Math.max(a.y0, b.y0),
|
|
731
|
+
x1: Math.min(a.x1, b.x1),
|
|
732
|
+
y1: Math.min(a.y1, b.y1)
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function clipConvexPolygon(src, n, stride, rect, dst, tmp) {
|
|
736
|
+
let count = clipEdge(src, n, stride, 0, rect.x0, 1, tmp);
|
|
737
|
+
count = clipEdge(tmp, count, stride, 0, rect.x1, -1, dst);
|
|
738
|
+
count = clipEdge(dst, count, stride, 1, rect.y0, 1, tmp);
|
|
739
|
+
return clipEdge(tmp, count, stride, 1, rect.y1, -1, dst);
|
|
740
|
+
}
|
|
741
|
+
function clipEdge(src, n, stride, axis, at, sign, out) {
|
|
742
|
+
if (n === 0) return 0;
|
|
743
|
+
let m = 0;
|
|
744
|
+
let prev = n - 1;
|
|
745
|
+
let prevIn = sign * (src[prev * stride + axis] - at) >= 0;
|
|
746
|
+
for (let cur = 0; cur < n; cur++) {
|
|
747
|
+
const curIn = sign * (src[cur * stride + axis] - at) >= 0;
|
|
748
|
+
if (curIn !== prevIn) {
|
|
749
|
+
const a = prev * stride;
|
|
750
|
+
const b = cur * stride;
|
|
751
|
+
const t = (at - src[a + axis]) / (src[b + axis] - src[a + axis]);
|
|
752
|
+
const o = m * stride;
|
|
753
|
+
for (let k = 0; k < stride; k++) out[o + k] = src[a + k] + t * (src[b + k] - src[a + k]);
|
|
754
|
+
out[o + axis] = at;
|
|
755
|
+
m++;
|
|
756
|
+
}
|
|
757
|
+
if (curIn) {
|
|
758
|
+
const o = m * stride;
|
|
759
|
+
const c = cur * stride;
|
|
760
|
+
for (let k = 0; k < stride; k++) out[o + k] = src[c + k];
|
|
761
|
+
m++;
|
|
762
|
+
}
|
|
763
|
+
prev = cur;
|
|
764
|
+
prevIn = curIn;
|
|
765
|
+
}
|
|
766
|
+
return m;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/drawBatch.ts
|
|
770
|
+
var MAX_VERTICES_PER_BATCH = 32768;
|
|
771
|
+
var FLOATS_PER_VERTEX = 10;
|
|
772
|
+
var INITIAL_VERTICES = 256;
|
|
773
|
+
var INITIAL_INDICES = 384;
|
|
774
|
+
var WHITE_U = 0.5;
|
|
775
|
+
var WHITE_V = 0.5;
|
|
776
|
+
var PLAIN_WHITE = packSlot(WHITE_SLOT, PAINT_MODE_PLAIN);
|
|
777
|
+
var SOLID_RING_SIZE = 64;
|
|
778
|
+
var BATCH_TIERS = [
|
|
779
|
+
{ vertices: 16, indices: 24 },
|
|
780
|
+
{ vertices: 1024, indices: 3072 }
|
|
781
|
+
];
|
|
782
|
+
var SOLID_RING_SLOT_VERTICES = BATCH_TIERS[BATCH_TIERS.length - 1].vertices;
|
|
783
|
+
var SOLID_LARGE_RING_SIZE = 4;
|
|
784
|
+
function doubledTo(from, need) {
|
|
785
|
+
let capacity = from;
|
|
786
|
+
while (capacity < need) capacity *= 2;
|
|
787
|
+
return capacity;
|
|
788
|
+
}
|
|
789
|
+
function quadIndices(quads) {
|
|
790
|
+
const out = new Uint32Array(quads * 6);
|
|
791
|
+
for (let q = 0; q < quads; q++) {
|
|
792
|
+
const b = q * 4;
|
|
793
|
+
const j = q * 6;
|
|
794
|
+
out[j] = b;
|
|
795
|
+
out[j + 1] = b + 1;
|
|
796
|
+
out[j + 2] = b + 2;
|
|
797
|
+
out[j + 3] = b;
|
|
798
|
+
out[j + 4] = b + 2;
|
|
799
|
+
out[j + 5] = b + 3;
|
|
800
|
+
}
|
|
801
|
+
return out;
|
|
802
|
+
}
|
|
803
|
+
var DrawBatch = class {
|
|
804
|
+
gl;
|
|
805
|
+
aPos;
|
|
806
|
+
aColor;
|
|
807
|
+
aUv;
|
|
808
|
+
aPost;
|
|
809
|
+
aSlot;
|
|
810
|
+
/** One ring per tier, cycled per flush. Slots are created on first use, so a
|
|
811
|
+
* renderer that flushes rarely allocates as few as it flushes. */
|
|
812
|
+
rings = BATCH_TIERS.map(() => new Array(SOLID_RING_SIZE).fill(void 0));
|
|
813
|
+
nextInRing = BATCH_TIERS.map(() => 0);
|
|
814
|
+
/** The same, for flushes past the largest tier; these sets grow to fit. */
|
|
815
|
+
largeRing = new Array(SOLID_LARGE_RING_SIZE).fill(void 0);
|
|
816
|
+
nextLarge = 0;
|
|
817
|
+
verts = new Float32Array(INITIAL_VERTICES * FLOATS_PER_VERTEX);
|
|
818
|
+
idx = new Uint32Array(INITIAL_INDICES);
|
|
819
|
+
nVerts = 0;
|
|
820
|
+
nIdx = 0;
|
|
821
|
+
/** Whether the staged run is quads alone, so its indices are the canonical
|
|
822
|
+
* pattern and a slot already holding that pattern needs no upload. */
|
|
823
|
+
pureRects = true;
|
|
824
|
+
/** The rect every push is cut to, or `null`. `draw.ts` sets it as rect
|
|
825
|
+
* clips enter and leave — see `batchClip.ts`. A push returns `false`, having
|
|
826
|
+
* staged nothing, for geometry it cannot cut exactly; see `clipQuad`. */
|
|
827
|
+
clip = null;
|
|
828
|
+
/** Polygon scratch for `clip`: a quad cut by four edges has at most eight
|
|
829
|
+
* corners. */
|
|
830
|
+
polyIn = new Float32Array(8 * FLOATS_PER_VERTEX);
|
|
831
|
+
polyOut = new Float32Array(8 * FLOATS_PER_VERTEX);
|
|
832
|
+
polyTmp = new Float32Array(8 * FLOATS_PER_VERTEX);
|
|
833
|
+
constructor(gl, prog) {
|
|
834
|
+
const aPos = prog.attribute("a_position");
|
|
835
|
+
const aColor = prog.attribute("a_vertexColor");
|
|
836
|
+
const aUv = prog.attribute("a_uv");
|
|
837
|
+
const aPost = prog.attribute("a_post");
|
|
838
|
+
const aSlot = prog.attribute("a_texSlot");
|
|
839
|
+
if (aPos === void 0 || aColor === void 0 || aUv === void 0 || aPost === void 0 || aSlot === void 0) {
|
|
840
|
+
throw new Error(
|
|
841
|
+
"DrawBatch: batch program is missing a_position / a_vertexColor / a_uv / a_post / a_texSlot"
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
this.gl = gl;
|
|
845
|
+
this.aPos = aPos;
|
|
846
|
+
this.aColor = aColor;
|
|
847
|
+
this.aUv = aUv;
|
|
848
|
+
this.aPost = aPost;
|
|
849
|
+
this.aSlot = aSlot;
|
|
850
|
+
}
|
|
851
|
+
get length() {
|
|
852
|
+
return this.nIdx;
|
|
853
|
+
}
|
|
854
|
+
/** Whether staging `vertices` more would put the run past the per-flush cap. */
|
|
855
|
+
wouldOverflow(vertices) {
|
|
856
|
+
return this.nVerts + vertices > MAX_VERTICES_PER_BATCH;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Append one rect's four corners through `m`, all carrying `rgba` (straight
|
|
860
|
+
* alpha). An affine maps a rect to a parallelogram, so two triangles still
|
|
861
|
+
* cover it and the batch draws at `u_model` identity.
|
|
862
|
+
*/
|
|
863
|
+
pushRect(x, y, w, h, m, r, g, b, a) {
|
|
864
|
+
this.reserve(4, 6);
|
|
865
|
+
let i = this.nVerts * FLOATS_PER_VERTEX;
|
|
866
|
+
const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
|
|
867
|
+
const x1 = x + w;
|
|
868
|
+
const y1 = y + h;
|
|
869
|
+
const ax = ma * x + mc * y + mtx, ay = mb * x + md * y + mty;
|
|
870
|
+
const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
|
|
871
|
+
const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
|
|
872
|
+
const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
|
|
873
|
+
const P = PLAIN_WHITE;
|
|
874
|
+
i = this.writeVertex(i, ax, ay, r, g, b, a, WHITE_U, WHITE_V, 1, P);
|
|
875
|
+
i = this.writeVertex(i, bx, by, r, g, b, a, WHITE_U, WHITE_V, 1, P);
|
|
876
|
+
i = this.writeVertex(i, cx, cy, r, g, b, a, WHITE_U, WHITE_V, 1, P);
|
|
877
|
+
this.writeVertex(i, dx, dy, r, g, b, a, WHITE_U, WHITE_V, 1, P);
|
|
878
|
+
this.pushQuadIndices();
|
|
879
|
+
return this.clip === null || this.clipQuad(this.nVerts - 4, this.nIdx - 6, this.clip);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Append one image quad: the destination rect `(x, y, w, h)` mapped through
|
|
883
|
+
* `m`, sampling `(u0, v0)`-`(u1, v1)`, every corner carrying `post` as the
|
|
884
|
+
* after-the-color-matrix alpha factor.
|
|
885
|
+
*
|
|
886
|
+
* Corners wind top-left, top-right, bottom-right, bottom-left with UVs
|
|
887
|
+
* following — the same winding `pushRect` uses, which is what lets a run of
|
|
888
|
+
* mixed rects and quads keep the canonical index pattern. The flips a command
|
|
889
|
+
* asks for are already in the `u` / `v` the caller passes.
|
|
890
|
+
*
|
|
891
|
+
* `slot` is the texture unit the corners sample, which `draw.ts` assigns per
|
|
892
|
+
* bitmap within the run.
|
|
893
|
+
*/
|
|
894
|
+
pushQuad(x, y, w, h, m, u0, v0, u1, v1, post, slot) {
|
|
895
|
+
this.reserve(4, 6);
|
|
896
|
+
let i = this.nVerts * FLOATS_PER_VERTEX;
|
|
897
|
+
const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
|
|
898
|
+
const x1 = x + w;
|
|
899
|
+
const y1 = y + h;
|
|
900
|
+
const ax = ma * x + mc * y + mtx, ay = mb * x + md * y + mty;
|
|
901
|
+
const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
|
|
902
|
+
const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
|
|
903
|
+
const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
|
|
904
|
+
const P = packSlot(slot, PAINT_MODE_PLAIN);
|
|
905
|
+
i = this.writeVertex(i, ax, ay, 1, 1, 1, 1, u0, v0, post, P);
|
|
906
|
+
i = this.writeVertex(i, bx, by, 1, 1, 1, 1, u1, v0, post, P);
|
|
907
|
+
i = this.writeVertex(i, cx, cy, 1, 1, 1, 1, u1, v1, post, P);
|
|
908
|
+
this.writeVertex(i, dx, dy, 1, 1, 1, 1, u0, v1, post, P);
|
|
909
|
+
this.pushQuadIndices();
|
|
910
|
+
return this.clip === null || this.clipQuad(this.nVerts - 4, this.nIdx - 6, this.clip);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Append one glyph quad: the box `(x0, y0)`-`(x1, y1)` mapped through `m`,
|
|
914
|
+
* sampling `(u0, v0)`-`(u1, v1)` of the font atlas at `slot`, painted in
|
|
915
|
+
* `rgba`.
|
|
916
|
+
*
|
|
917
|
+
* `mode` says which channels of that atlas carry the distance field, and
|
|
918
|
+
* rides the vertices packed into the slot — so a run off a baked MSDF atlas
|
|
919
|
+
* and one off the runtime canvas bake still share a draw. The synthetic-bold
|
|
920
|
+
* threshold does not: it is `u_synthBold`, and `draw.ts` breaks the run when
|
|
921
|
+
* it changes.
|
|
922
|
+
*
|
|
923
|
+
* **A synthetic oblique is sheared here rather than in the shader.** The old
|
|
924
|
+
* text program carried the baseline per vertex and skewed against
|
|
925
|
+
* `u_synthItalic`; the batch places its own corners, so the shear is one
|
|
926
|
+
* multiply while they are being placed, and `tanItalic` is 0 for an upright
|
|
927
|
+
* face. It has to happen before `m`, which is where the shader had it too.
|
|
928
|
+
*
|
|
929
|
+
* Corners wind top-left, top-right, bottom-right, bottom-left — `pushQuad`'s
|
|
930
|
+
* winding, which is what lets a run of mixed rects, image quads and glyphs
|
|
931
|
+
* keep the canonical index pattern.
|
|
932
|
+
*/
|
|
933
|
+
pushGlyph(x0, y0, x1, y1, baselineY, tanItalic, m, u0, v0, u1, v1, r, g, b, a, slot, mode) {
|
|
934
|
+
this.reserve(4, 6);
|
|
935
|
+
let i = this.nVerts * FLOATS_PER_VERTEX;
|
|
936
|
+
const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
|
|
937
|
+
const topShift = (baselineY - y0) * tanItalic;
|
|
938
|
+
const botShift = (baselineY - y1) * tanItalic;
|
|
939
|
+
const tx0 = x0 + topShift, tx1 = x1 + topShift;
|
|
940
|
+
const bx0 = x0 + botShift, bx1 = x1 + botShift;
|
|
941
|
+
const ax = ma * tx0 + mc * y0 + mtx, ay = mb * tx0 + md * y0 + mty;
|
|
942
|
+
const bx = ma * tx1 + mc * y0 + mtx, by = mb * tx1 + md * y0 + mty;
|
|
943
|
+
const cx = ma * bx1 + mc * y1 + mtx, cy = mb * bx1 + md * y1 + mty;
|
|
944
|
+
const dx = ma * bx0 + mc * y1 + mtx, dy = mb * bx0 + md * y1 + mty;
|
|
945
|
+
const P = packSlot(slot, mode);
|
|
946
|
+
i = this.writeVertex(i, ax, ay, r, g, b, a, u0, v0, 1, P);
|
|
947
|
+
i = this.writeVertex(i, bx, by, r, g, b, a, u1, v0, 1, P);
|
|
948
|
+
i = this.writeVertex(i, cx, cy, r, g, b, a, u1, v1, 1, P);
|
|
949
|
+
this.writeVertex(i, dx, dy, r, g, b, a, u0, v1, 1, P);
|
|
950
|
+
this.pushQuadIndices();
|
|
951
|
+
return this.clip === null || this.clipQuad(this.nVerts - 4, this.nIdx - 6, this.clip);
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Append one rect filled by a gradient: the corners of `(x, y, w, h)` mapped
|
|
955
|
+
* through `m`, sampling the ramp atlas at `slot`.
|
|
956
|
+
*
|
|
957
|
+
* `uv` gives each channel of `a_uv` as an affine function of the coordinates
|
|
958
|
+
* the corners arrive in, which is what lets a gradient ride the vertices at
|
|
959
|
+
* all: a linear one's ramp position is affine in position outright, and a
|
|
960
|
+
* radial or conic one's gradient-space *coordinate* is, even though the ramp
|
|
961
|
+
* position it yields is not. Either way the rasterizer's interpolation across
|
|
962
|
+
* a triangle is exact. `mode` says which of the two the fragment shader is
|
|
963
|
+
* looking at, and `post` carries the atlas row for the modes that read it —
|
|
964
|
+
* see `shaders/batchFill.ts`.
|
|
965
|
+
*
|
|
966
|
+
* Values outside 0..1 are the sampler's business; the atlas clamps to the
|
|
967
|
+
* edge texel, which is what the gradient shader's own `clamp` did.
|
|
968
|
+
*/
|
|
969
|
+
pushGradientRect(x, y, w, h, m, uv, post, slot, mode, r, g, b, a) {
|
|
970
|
+
this.reserve(4, 6);
|
|
971
|
+
let i = this.nVerts * FLOATS_PER_VERTEX;
|
|
972
|
+
const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
|
|
973
|
+
const x1 = x + w;
|
|
974
|
+
const y1 = y + h;
|
|
975
|
+
const P = packSlot(slot, mode);
|
|
976
|
+
const corner = (cx, cy) => {
|
|
977
|
+
const at = i;
|
|
978
|
+
i = this.writeVertex(
|
|
979
|
+
at,
|
|
980
|
+
ma * cx + mc * cy + mtx,
|
|
981
|
+
mb * cx + md * cy + mty,
|
|
982
|
+
r,
|
|
983
|
+
g,
|
|
984
|
+
b,
|
|
985
|
+
a,
|
|
986
|
+
uv.ux * cx + uv.uy * cy + uv.u0,
|
|
987
|
+
uv.vx * cx + uv.vy * cy + uv.v0,
|
|
988
|
+
post,
|
|
989
|
+
P
|
|
990
|
+
);
|
|
991
|
+
return i;
|
|
992
|
+
};
|
|
993
|
+
corner(x, y);
|
|
994
|
+
corner(x1, y);
|
|
995
|
+
corner(x1, y1);
|
|
996
|
+
corner(x, y1);
|
|
997
|
+
this.pushQuadIndices();
|
|
998
|
+
return this.clip === null || this.clipQuad(this.nVerts - 4, this.nIdx - 6, this.clip);
|
|
999
|
+
}
|
|
1000
|
+
/** `pushMesh` for a mesh filled by a gradient — see `pushGradientRect` for
|
|
1001
|
+
* what `uv`, `post` and `mode` carry. */
|
|
1002
|
+
pushGradientMesh(mesh, m, uv, post, slot, mode, r, g, b, a) {
|
|
1003
|
+
const src = mesh.vertices;
|
|
1004
|
+
const srcIdx = mesh.indices;
|
|
1005
|
+
const n = src.length >> 1;
|
|
1006
|
+
this.reserve(n, srcIdx.length);
|
|
1007
|
+
this.pureRects = false;
|
|
1008
|
+
const v = this.verts;
|
|
1009
|
+
let i = this.nVerts * FLOATS_PER_VERTEX;
|
|
1010
|
+
const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
|
|
1011
|
+
const P = packSlot(slot, mode);
|
|
1012
|
+
for (let k = 0; k < n; k++) {
|
|
1013
|
+
const x = src[k * 2];
|
|
1014
|
+
const y = src[k * 2 + 1];
|
|
1015
|
+
v[i++] = ma * x + mc * y + mtx;
|
|
1016
|
+
v[i++] = mb * x + md * y + mty;
|
|
1017
|
+
v[i++] = r;
|
|
1018
|
+
v[i++] = g;
|
|
1019
|
+
v[i++] = b;
|
|
1020
|
+
v[i++] = a;
|
|
1021
|
+
v[i++] = uv.ux * x + uv.uy * y + uv.u0;
|
|
1022
|
+
v[i++] = uv.vx * x + uv.vy * y + uv.v0;
|
|
1023
|
+
v[i++] = post;
|
|
1024
|
+
v[i++] = P;
|
|
1025
|
+
}
|
|
1026
|
+
const base = this.nVerts;
|
|
1027
|
+
const out = this.idx;
|
|
1028
|
+
let j = this.nIdx;
|
|
1029
|
+
for (let k = 0; k < srcIdx.length; k++) out[j++] = base + srcIdx[k];
|
|
1030
|
+
this.nVerts += n;
|
|
1031
|
+
this.nIdx += srcIdx.length;
|
|
1032
|
+
return this.clip === null || this.clipMesh(base, this.nIdx - srcIdx.length, this.clip);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Append a tessellated mesh through `m`, all vertices carrying `rgba`. The
|
|
1036
|
+
* mesh's own indices are rebased onto the staged vertices, which is why the
|
|
1037
|
+
* index buffer is uploaded per flush rather than written once.
|
|
1038
|
+
*/
|
|
1039
|
+
pushMesh(mesh, m, r, g, b, a) {
|
|
1040
|
+
const src = mesh.vertices;
|
|
1041
|
+
const srcIdx = mesh.indices;
|
|
1042
|
+
const n = src.length >> 1;
|
|
1043
|
+
this.reserve(n, srcIdx.length);
|
|
1044
|
+
this.pureRects = false;
|
|
1045
|
+
const v = this.verts;
|
|
1046
|
+
let i = this.nVerts * FLOATS_PER_VERTEX;
|
|
1047
|
+
const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
|
|
1048
|
+
for (let k = 0; k < n; k++) {
|
|
1049
|
+
const x = src[k * 2];
|
|
1050
|
+
const y = src[k * 2 + 1];
|
|
1051
|
+
v[i++] = ma * x + mc * y + mtx;
|
|
1052
|
+
v[i++] = mb * x + md * y + mty;
|
|
1053
|
+
v[i++] = r;
|
|
1054
|
+
v[i++] = g;
|
|
1055
|
+
v[i++] = b;
|
|
1056
|
+
v[i++] = a;
|
|
1057
|
+
v[i++] = WHITE_U;
|
|
1058
|
+
v[i++] = WHITE_V;
|
|
1059
|
+
v[i++] = 1;
|
|
1060
|
+
v[i++] = PLAIN_WHITE;
|
|
1061
|
+
}
|
|
1062
|
+
const base = this.nVerts;
|
|
1063
|
+
const out = this.idx;
|
|
1064
|
+
let j = this.nIdx;
|
|
1065
|
+
for (let k = 0; k < srcIdx.length; k++) out[j++] = base + srcIdx[k];
|
|
1066
|
+
this.nVerts += n;
|
|
1067
|
+
this.nIdx += srcIdx.length;
|
|
1068
|
+
return this.clip === null || this.clipMesh(base, this.nIdx - srcIdx.length, this.clip);
|
|
1069
|
+
}
|
|
1070
|
+
/** Upload the staged geometry into the next set of buffers and bind its VAO.
|
|
1071
|
+
* Returns the index count for the caller's `drawElements`. */
|
|
1072
|
+
uploadAndBind() {
|
|
1073
|
+
const gl = this.gl;
|
|
1074
|
+
const tier = BATCH_TIERS.findIndex(
|
|
1075
|
+
(t) => this.nVerts <= t.vertices && this.nIdx <= t.indices
|
|
1076
|
+
);
|
|
1077
|
+
const set = tier < 0 ? this.nextLargeSlot() : this.nextRingSlot(tier);
|
|
1078
|
+
gl.bindVertexArray(set.vao);
|
|
1079
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, set.vbo);
|
|
1080
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nVerts * FLOATS_PER_VERTEX);
|
|
1081
|
+
if (!this.pureRects) {
|
|
1082
|
+
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, this.idx, 0, this.nIdx);
|
|
1083
|
+
set.canonicalQuads = 0;
|
|
1084
|
+
} else if (set.canonicalQuads < this.nVerts >> 2) {
|
|
1085
|
+
const quads = Math.min(set.vertexCapacity >> 2, Math.floor(set.indexCapacity / 6));
|
|
1086
|
+
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, quadIndices(quads));
|
|
1087
|
+
set.canonicalQuads = quads;
|
|
1088
|
+
}
|
|
1089
|
+
return this.nIdx;
|
|
1090
|
+
}
|
|
1091
|
+
reset() {
|
|
1092
|
+
this.nVerts = 0;
|
|
1093
|
+
this.nIdx = 0;
|
|
1094
|
+
this.pureRects = true;
|
|
1095
|
+
}
|
|
1096
|
+
dispose() {
|
|
1097
|
+
for (const set of [...this.rings.flat(), ...this.largeRing]) {
|
|
1098
|
+
if (set) this.deleteSet(set);
|
|
1099
|
+
}
|
|
1100
|
+
for (const ring of this.rings) ring.fill(void 0);
|
|
1101
|
+
this.largeRing.fill(void 0);
|
|
1102
|
+
}
|
|
1103
|
+
writeVertex(i, x, y, r, g, b, a, u, v, post, packed) {
|
|
1104
|
+
const out = this.verts;
|
|
1105
|
+
out[i] = x;
|
|
1106
|
+
out[i + 1] = y;
|
|
1107
|
+
out[i + 2] = r;
|
|
1108
|
+
out[i + 3] = g;
|
|
1109
|
+
out[i + 4] = b;
|
|
1110
|
+
out[i + 5] = a;
|
|
1111
|
+
out[i + 6] = u;
|
|
1112
|
+
out[i + 7] = v;
|
|
1113
|
+
out[i + 8] = post;
|
|
1114
|
+
out[i + 9] = packed;
|
|
1115
|
+
return i + FLOATS_PER_VERTEX;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Fit the quad just written at `v0` to `rect`: keep it if it lies inside,
|
|
1119
|
+
* drop it if it misses, and cut it if it crosses — but only if its edges
|
|
1120
|
+
* run along the axes. Cutting a slanted edge moves its endpoint off the
|
|
1121
|
+
* subpixel grid the rasterizer snaps to, and the shortened edge then covers
|
|
1122
|
+
* different samples than the whole one did; along an axis the cut edge is
|
|
1123
|
+
* the same line exactly. So a slanted quad that crosses is rolled back and
|
|
1124
|
+
* refused, for `draw.ts` to clip through the stencil instead.
|
|
1125
|
+
*
|
|
1126
|
+
* A cut axis-aligned quad is still four corners, so the run stays pure.
|
|
1127
|
+
*/
|
|
1128
|
+
clipQuad(v0, i0, rect) {
|
|
1129
|
+
const v = this.verts;
|
|
1130
|
+
const at = v0 * FLOATS_PER_VERTEX;
|
|
1131
|
+
const F = FLOATS_PER_VERTEX;
|
|
1132
|
+
const ax = v[at], ay = v[at + 1], bx = v[at + F], by = v[at + F + 1];
|
|
1133
|
+
const cx = v[at + 2 * F], cy = v[at + 2 * F + 1], dx = v[at + 3 * F], dy = v[at + 3 * F + 1];
|
|
1134
|
+
const minX = Math.min(ax, bx, cx, dx), maxX = Math.max(ax, bx, cx, dx);
|
|
1135
|
+
const minY = Math.min(ay, by, cy, dy), maxY = Math.max(ay, by, cy, dy);
|
|
1136
|
+
if (minX >= rect.x0 && maxX <= rect.x1 && minY >= rect.y0 && maxY <= rect.y1) return true;
|
|
1137
|
+
this.nVerts = v0;
|
|
1138
|
+
this.nIdx = i0;
|
|
1139
|
+
if (maxX <= rect.x0 || minX >= rect.x1 || maxY <= rect.y0 || minY >= rect.y1) return true;
|
|
1140
|
+
const alongAxes = ay === by && bx === cx && cy === dy && dx === ax || ax === bx && by === cy && cx === dx && dy === ay;
|
|
1141
|
+
if (!alongAxes) return false;
|
|
1142
|
+
this.polyIn.set(v.subarray(at, at + 4 * F));
|
|
1143
|
+
const n = clipConvexPolygon(this.polyIn, 4, F, rect, this.polyOut, this.polyTmp);
|
|
1144
|
+
this.appendFan(this.polyOut, n);
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
/** `clipQuad` for a mesh staged from `v0`. A mesh's edges are slanted in
|
|
1148
|
+
* general, so one that crosses is refused rather than cut. */
|
|
1149
|
+
clipMesh(v0, i0, rect) {
|
|
1150
|
+
const v = this.verts;
|
|
1151
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1152
|
+
for (let i = v0 * FLOATS_PER_VERTEX; i < this.nVerts * FLOATS_PER_VERTEX; i += FLOATS_PER_VERTEX) {
|
|
1153
|
+
const x = v[i];
|
|
1154
|
+
const y = v[i + 1];
|
|
1155
|
+
if (x < minX) minX = x;
|
|
1156
|
+
if (x > maxX) maxX = x;
|
|
1157
|
+
if (y < minY) minY = y;
|
|
1158
|
+
if (y > maxY) maxY = y;
|
|
1159
|
+
}
|
|
1160
|
+
if (minX >= rect.x0 && maxX <= rect.x1 && minY >= rect.y0 && maxY <= rect.y1) return true;
|
|
1161
|
+
this.nVerts = v0;
|
|
1162
|
+
this.nIdx = i0;
|
|
1163
|
+
return maxX <= rect.x0 || minX >= rect.x1 || maxY <= rect.y0 || minY >= rect.y1;
|
|
1164
|
+
}
|
|
1165
|
+
/** Append the `n`-corner convex polygon in `poly` as a triangle fan. */
|
|
1166
|
+
appendFan(poly, n) {
|
|
1167
|
+
if (n < 3) return;
|
|
1168
|
+
this.reserve(n, (n - 2) * 3);
|
|
1169
|
+
if (n !== 4) this.pureRects = false;
|
|
1170
|
+
const base = this.nVerts;
|
|
1171
|
+
this.verts.set(poly.subarray(0, n * FLOATS_PER_VERTEX), base * FLOATS_PER_VERTEX);
|
|
1172
|
+
let j = this.nIdx;
|
|
1173
|
+
for (let k = 1; k < n - 1; k++) {
|
|
1174
|
+
this.idx[j++] = base;
|
|
1175
|
+
this.idx[j++] = base + k;
|
|
1176
|
+
this.idx[j++] = base + k + 1;
|
|
1177
|
+
}
|
|
1178
|
+
this.nVerts += n;
|
|
1179
|
+
this.nIdx = j;
|
|
1180
|
+
}
|
|
1181
|
+
/** Two triangles over the four corners just written. */
|
|
1182
|
+
pushQuadIndices() {
|
|
1183
|
+
const base = this.nVerts;
|
|
1184
|
+
let j = this.nIdx;
|
|
1185
|
+
this.idx[j++] = base;
|
|
1186
|
+
this.idx[j++] = base + 1;
|
|
1187
|
+
this.idx[j++] = base + 2;
|
|
1188
|
+
this.idx[j++] = base;
|
|
1189
|
+
this.idx[j++] = base + 2;
|
|
1190
|
+
this.idx[j++] = base + 3;
|
|
1191
|
+
this.nVerts += 4;
|
|
1192
|
+
this.nIdx += 6;
|
|
1193
|
+
}
|
|
1194
|
+
/** Grow the CPU arrays so `vertices` / `indices` more fit. */
|
|
1195
|
+
reserve(vertices, indices) {
|
|
1196
|
+
const needVerts = (this.nVerts + vertices) * FLOATS_PER_VERTEX;
|
|
1197
|
+
if (needVerts > this.verts.length) {
|
|
1198
|
+
const grown = new Float32Array(doubledTo(this.verts.length, needVerts));
|
|
1199
|
+
grown.set(this.verts);
|
|
1200
|
+
this.verts = grown;
|
|
1201
|
+
}
|
|
1202
|
+
const needIdx = this.nIdx + indices;
|
|
1203
|
+
if (needIdx > this.idx.length) {
|
|
1204
|
+
const grown = new Uint32Array(doubledTo(this.idx.length, needIdx));
|
|
1205
|
+
grown.set(this.idx);
|
|
1206
|
+
this.idx = grown;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
nextRingSlot(tier) {
|
|
1210
|
+
const ring = this.rings[tier];
|
|
1211
|
+
const slot = this.nextInRing[tier];
|
|
1212
|
+
this.nextInRing[tier] = (slot + 1) % ring.length;
|
|
1213
|
+
const existing = ring[slot];
|
|
1214
|
+
if (existing) return existing;
|
|
1215
|
+
const set = this.createSet(BATCH_TIERS[tier].vertices, BATCH_TIERS[tier].indices);
|
|
1216
|
+
ring[slot] = set;
|
|
1217
|
+
return set;
|
|
1218
|
+
}
|
|
1219
|
+
nextLargeSlot() {
|
|
1220
|
+
const gl = this.gl;
|
|
1221
|
+
const slot = this.nextLarge;
|
|
1222
|
+
this.nextLarge = (slot + 1) % SOLID_LARGE_RING_SIZE;
|
|
1223
|
+
let set = this.largeRing[slot];
|
|
1224
|
+
if (!set) {
|
|
1225
|
+
set = this.createSet(
|
|
1226
|
+
doubledTo(SOLID_RING_SLOT_VERTICES, this.nVerts),
|
|
1227
|
+
doubledTo(BATCH_TIERS[BATCH_TIERS.length - 1].indices, this.nIdx)
|
|
1228
|
+
);
|
|
1229
|
+
this.largeRing[slot] = set;
|
|
1230
|
+
return set;
|
|
1231
|
+
}
|
|
1232
|
+
if (this.nVerts > set.vertexCapacity) {
|
|
1233
|
+
set.vertexCapacity = doubledTo(set.vertexCapacity, this.nVerts);
|
|
1234
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, set.vbo);
|
|
1235
|
+
gl.bufferData(gl.ARRAY_BUFFER, set.vertexCapacity * FLOATS_PER_VERTEX * 4, gl.DYNAMIC_DRAW);
|
|
1236
|
+
}
|
|
1237
|
+
if (this.nIdx > set.indexCapacity) {
|
|
1238
|
+
set.indexCapacity = doubledTo(set.indexCapacity, this.nIdx);
|
|
1239
|
+
gl.bindVertexArray(set.vao);
|
|
1240
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, set.ibo);
|
|
1241
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, set.indexCapacity * 4, gl.DYNAMIC_DRAW);
|
|
1242
|
+
gl.bindVertexArray(null);
|
|
1243
|
+
set.canonicalQuads = 0;
|
|
1244
|
+
}
|
|
1245
|
+
return set;
|
|
1246
|
+
}
|
|
1247
|
+
createSet(vertices, indices) {
|
|
1248
|
+
const gl = this.gl;
|
|
1249
|
+
const vao = gl.createVertexArray();
|
|
1250
|
+
const vbo = gl.createBuffer();
|
|
1251
|
+
const ibo = gl.createBuffer();
|
|
1252
|
+
if (!vao || !vbo || !ibo) throw new Error("DrawBatch: failed to create GL objects");
|
|
1253
|
+
const stride = FLOATS_PER_VERTEX * 4;
|
|
1254
|
+
gl.bindVertexArray(vao);
|
|
1255
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
|
1256
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices * stride, gl.DYNAMIC_DRAW);
|
|
1257
|
+
gl.enableVertexAttribArray(this.aPos);
|
|
1258
|
+
gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, stride, 0);
|
|
1259
|
+
gl.enableVertexAttribArray(this.aColor);
|
|
1260
|
+
gl.vertexAttribPointer(this.aColor, 4, gl.FLOAT, false, stride, 8);
|
|
1261
|
+
gl.enableVertexAttribArray(this.aUv);
|
|
1262
|
+
gl.vertexAttribPointer(this.aUv, 2, gl.FLOAT, false, stride, 24);
|
|
1263
|
+
gl.enableVertexAttribArray(this.aPost);
|
|
1264
|
+
gl.vertexAttribPointer(this.aPost, 1, gl.FLOAT, false, stride, 32);
|
|
1265
|
+
gl.enableVertexAttribArray(this.aSlot);
|
|
1266
|
+
gl.vertexAttribPointer(this.aSlot, 1, gl.FLOAT, false, stride, 36);
|
|
1267
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
|
|
1268
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices * 4, gl.DYNAMIC_DRAW);
|
|
1269
|
+
gl.bindVertexArray(null);
|
|
1270
|
+
return { vao, vbo, ibo, vertexCapacity: vertices, indexCapacity: indices, canonicalQuads: 0 };
|
|
1271
|
+
}
|
|
1272
|
+
deleteSet(set) {
|
|
1273
|
+
const gl = this.gl;
|
|
1274
|
+
gl.deleteBuffer(set.vbo);
|
|
1275
|
+
gl.deleteBuffer(set.ibo);
|
|
1276
|
+
gl.deleteVertexArray(set.vao);
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
// src/images/imageCache.ts
|
|
1281
|
+
var MAX_VECTOR_RASTER_PIXELS = 4096 * 4096;
|
|
1282
|
+
var SHRINK_SLACK = 4;
|
|
1283
|
+
var FALLBACK_WIDTH = 300;
|
|
1284
|
+
var FALLBACK_HEIGHT = 150;
|
|
1285
|
+
var cache6 = /* @__PURE__ */ new Map();
|
|
1286
|
+
var subscribers = /* @__PURE__ */ new Set();
|
|
1287
|
+
var decodedVectors = /* @__PURE__ */ new Map();
|
|
1288
|
+
function isVectorImageSrc(src) {
|
|
1289
|
+
if (/^data:image\/svg\+xml[;,]/i.test(src)) return true;
|
|
1290
|
+
if (/^(data|blob):/i.test(src)) return false;
|
|
1291
|
+
return /\.svgz?$/i.test(src.replace(/[?#].*$/, ""));
|
|
1292
|
+
}
|
|
1293
|
+
function requireDom() {
|
|
1294
|
+
if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
|
|
1295
|
+
throw new Error("weasel imageCache: no DOM image loader in this environment");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
async function decodeImage(src) {
|
|
1299
|
+
const img = new Image();
|
|
1300
|
+
img.crossOrigin = "anonymous";
|
|
1301
|
+
img.src = src;
|
|
1302
|
+
await img.decode();
|
|
1303
|
+
return img;
|
|
1304
|
+
}
|
|
1305
|
+
function decodeVector(src) {
|
|
1306
|
+
let decoded = decodedVectors.get(src);
|
|
1307
|
+
if (!decoded) {
|
|
1308
|
+
decoded = decodeImage(src);
|
|
1309
|
+
decodedVectors.set(src, decoded);
|
|
1310
|
+
decoded.catch(() => decodedVectors.delete(src));
|
|
1311
|
+
}
|
|
1312
|
+
return decoded;
|
|
1313
|
+
}
|
|
1314
|
+
async function rasterizeVector(src, size) {
|
|
1315
|
+
const img = await decodeVector(src);
|
|
1316
|
+
const width = size?.width ?? (img.naturalWidth || FALLBACK_WIDTH);
|
|
1317
|
+
const height = size?.height ?? (img.naturalHeight || FALLBACK_HEIGHT);
|
|
1318
|
+
const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), { width, height });
|
|
1319
|
+
const c2d = canvas.getContext("2d");
|
|
1320
|
+
if (!c2d) throw new Error("weasel imageCache: no 2D context to rasterize an SVG");
|
|
1321
|
+
c2d.drawImage(img, 0, 0, width, height);
|
|
1322
|
+
return createImageBitmap(canvas);
|
|
1323
|
+
}
|
|
1324
|
+
var defaultLoader = async (src, size) => {
|
|
1325
|
+
requireDom();
|
|
1326
|
+
if (isVectorImageSrc(src)) return rasterizeVector(src, size);
|
|
1327
|
+
return createImageBitmap(await decodeImage(src));
|
|
1328
|
+
};
|
|
1329
|
+
var loader = defaultLoader;
|
|
1330
|
+
function notify() {
|
|
1331
|
+
for (const cb of subscribers) cb();
|
|
1332
|
+
}
|
|
1333
|
+
function startLoad2(src) {
|
|
1334
|
+
const entry = { status: "loading" };
|
|
1335
|
+
cache6.set(src, entry);
|
|
1336
|
+
loader(src).then(
|
|
1337
|
+
(bitmap) => {
|
|
1338
|
+
if (cache6.get(src) !== entry) return;
|
|
1339
|
+
cache6.set(src, {
|
|
1340
|
+
status: "ready",
|
|
1341
|
+
bitmap,
|
|
1342
|
+
...isVectorImageSrc(src) ? { vector: { natural: { width: bitmap.width, height: bitmap.height }, scale: 1, failed: /* @__PURE__ */ new Set(), floor: 0 } } : {}
|
|
1343
|
+
});
|
|
1344
|
+
notify();
|
|
1345
|
+
},
|
|
1346
|
+
() => {
|
|
1347
|
+
if (cache6.get(src) !== entry) return;
|
|
1348
|
+
cache6.set(src, { status: "error" });
|
|
1349
|
+
notify();
|
|
1350
|
+
}
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
function rasterScaleFor(natural, drawnAt) {
|
|
1354
|
+
const need = Math.max(drawnAt.width / natural.width, drawnAt.height / natural.height);
|
|
1355
|
+
if (!(need > 0) || !Number.isFinite(need)) return void 0;
|
|
1356
|
+
const side = maxImageTextureSide();
|
|
1357
|
+
const cap = Math.min(
|
|
1358
|
+
side / natural.width,
|
|
1359
|
+
side / natural.height,
|
|
1360
|
+
Math.sqrt(MAX_VECTOR_RASTER_PIXELS / (natural.width * natural.height))
|
|
1361
|
+
);
|
|
1362
|
+
return Math.min(2 ** Math.ceil(Math.log2(need)), cap);
|
|
1363
|
+
}
|
|
1364
|
+
function maybeReraster(src, entry, v, drawnAt) {
|
|
1365
|
+
if (v.pending !== void 0) return;
|
|
1366
|
+
const wanted = rasterScaleFor(v.natural, drawnAt);
|
|
1367
|
+
if (wanted === void 0) return;
|
|
1368
|
+
const grow = wanted > v.scale;
|
|
1369
|
+
if (grow && v.shrunkTo === v.scale) v.floor = Math.max(v.floor, wanted);
|
|
1370
|
+
const target = grow ? wanted : Math.max(wanted, v.floor);
|
|
1371
|
+
if (!grow && target * SHRINK_SLACK > v.scale) return;
|
|
1372
|
+
for (const f of v.failed) if (target === f || grow && f > v.scale && target >= f) return;
|
|
1373
|
+
v.pending = target;
|
|
1374
|
+
const size = {
|
|
1375
|
+
width: Math.max(1, Math.round(v.natural.width * target)),
|
|
1376
|
+
height: Math.max(1, Math.round(v.natural.height * target))
|
|
1377
|
+
};
|
|
1378
|
+
loader(src, size).then(
|
|
1379
|
+
(bitmap) => {
|
|
1380
|
+
if (cache6.get(src) !== entry) return;
|
|
1381
|
+
v.pending = void 0;
|
|
1382
|
+
const old = entry.bitmap;
|
|
1383
|
+
entry.bitmap = bitmap;
|
|
1384
|
+
v.shrunkTo = grow ? void 0 : target;
|
|
1385
|
+
v.scale = target;
|
|
1386
|
+
if (old && old !== bitmap) releaseImageSource(old);
|
|
1387
|
+
notify();
|
|
1388
|
+
},
|
|
1389
|
+
() => {
|
|
1390
|
+
if (cache6.get(src) !== entry) return;
|
|
1391
|
+
v.pending = void 0;
|
|
1392
|
+
v.failed.add(target);
|
|
1393
|
+
}
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
function getImageBitmap(src, drawnAt) {
|
|
1397
|
+
const entry = cache6.get(src);
|
|
1398
|
+
if (!entry) {
|
|
1399
|
+
startLoad2(src);
|
|
1400
|
+
return void 0;
|
|
1401
|
+
}
|
|
1402
|
+
if (drawnAt && entry.vector) maybeReraster(src, entry, entry.vector, drawnAt);
|
|
1403
|
+
return entry.bitmap;
|
|
1404
|
+
}
|
|
1405
|
+
function imageStatus(src) {
|
|
1406
|
+
return cache6.get(src)?.status ?? "idle";
|
|
1407
|
+
}
|
|
1408
|
+
function releaseImage(src) {
|
|
1409
|
+
const entry = cache6.get(src);
|
|
1410
|
+
cache6.delete(src);
|
|
1411
|
+
decodedVectors.delete(src);
|
|
1412
|
+
if (entry?.bitmap) releaseImageSource(entry.bitmap);
|
|
1413
|
+
}
|
|
1414
|
+
function retryImage(src) {
|
|
1415
|
+
if (cache6.get(src)?.status !== "error") return;
|
|
1416
|
+
startLoad2(src);
|
|
1417
|
+
notify();
|
|
1418
|
+
}
|
|
1419
|
+
function subscribeImageReady(cb) {
|
|
1420
|
+
subscribers.add(cb);
|
|
1421
|
+
return () => {
|
|
1422
|
+
subscribers.delete(cb);
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function __setImageLoaderForTests(fn) {
|
|
1426
|
+
loader = fn;
|
|
1427
|
+
}
|
|
1428
|
+
function _resetImageCacheForTests() {
|
|
1429
|
+
cache6.clear();
|
|
1430
|
+
subscribers.clear();
|
|
1431
|
+
decodedVectors.clear();
|
|
1432
|
+
loader = defaultLoader;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
export { BATCH_ATTRIBUTE_LOCATIONS, BATCH_FILL_ATTRIBUTES, BATCH_FILL_UNIFORMS, BATCH_FRAG_SRC, BATCH_TEXTURE_SLOTS, BATCH_VERT_SRC, DrawBatch, FLOATS_PER_VERTEX, GLImageCache, PAINT_MODE_CONIC, PAINT_MODE_PLAIN, PAINT_MODE_RADIAL, __setImageLoaderForTests, _resetCacheForTests, _resetImageCacheForTests, _resetMarkerCommandCacheForTests, _resetOutlineMeshCacheForTests, _resetOutlineStrokeMeshCacheForTests, _resetPaintDrawKindsForTests, _resetPatternSpecCacheForTests, _resetProgramRegistryForTests, _resetStrokeMeshCacheForTests, axisAlignedClipRect, cachedMarkerCommands, commandFill, commandStroke, fillInPoseFrame, fillToBoundsFrame, getImageBitmap, getMesh, getPaintDrawKind, getProgramSource, imageStatus, intersectClipRects, isPaintKindKnown, isPatternSpec, isVectorImageSrc, outlineMesh, outlineStrokeMesh, paintDrawKindRegistry, programSourceRegistry, quantizeEmWidth, quantizeStrokeScale, registerPaintDrawKind, registerPaintDrawKindLoader, registerProgram, releaseImage, releaseImageSource, resolveFillPattern, resolvePatternSpec, retryImage, strokeMesh, subscribeImageReady, warmPaintKinds };
|
|
1436
|
+
//# sourceMappingURL=chunk-VHQJ56XQ.js.map
|
|
1437
|
+
//# sourceMappingURL=chunk-VHQJ56XQ.js.map
|