@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.
@@ -0,0 +1,120 @@
1
+ import { Path } from '@weasel-js/geom';
2
+ import { Mesh } from '@weasel-js/geom/tessellate';
3
+ export { F as FLOATS_PER_VERTEX, _ as __setImageLoaderForTests, k as _resetImageCacheForTests, l as _resetPatternSpecCacheForTests, m as _resetTextureRegistryForTests } from './imageCache-oJVgxhkr.js';
4
+ export { _ as _resetPaintDrawKindsForTests, s as _resetProgramRegistryForTests } from './paintKinds-B2vEy9A4.js';
5
+ import '@weasel-js/paint';
6
+ import '@weasel-js/registry';
7
+
8
+ /**
9
+ * Return the tessellated Mesh for `path`, computing and caching on first call.
10
+ * Cached by Path identity (WeakMap) — different Path objects with the same
11
+ * coords are distinct cache entries.
12
+ *
13
+ * A rect path the batch stages as a quad never reaches this cache. One used
14
+ * as a clip, or filled with a pattern or vertex colors, does.
15
+ *
16
+ * Also bypassed whenever `DrawContext.flattenTolerance` is set: `fillMeshHandle`
17
+ * (see `../draw`) routes custom-tolerance fills through a fresh transient
18
+ * tessellation instead of this cache.
19
+ */
20
+ declare function getMesh(path: Path): Mesh;
21
+ /** Test helper. Do not call from product code. */
22
+ declare function _resetCacheForTests(): void;
23
+
24
+ /**
25
+ * A stroke's marker commands, cached per path. The placed head is a fresh
26
+ * `Path`, and the fill cache keys on path identity — so rebuilding it every
27
+ * frame would re-tessellate and re-upload every head every frame.
28
+ *
29
+ * Keyed on value, so a painter that builds a new stroke object every frame
30
+ * still hits: the resolved lengths, the flatten tolerance, the registry's
31
+ * generation, the marker refs and whatever fields each head declares it reads.
32
+ * A head that does not declare them may read any field, so for it the stroke
33
+ * as the command carried it — before any `{ px }` length was resolved — joins
34
+ * the key by identity. Paint is applied over the cached geometry, so a new
35
+ * paint reuses the heads.
36
+ */
37
+
38
+ /** Test helper. Do not call from product code. */
39
+ declare function _resetMarkerCommandCacheForTests(): void;
40
+
41
+ /**
42
+ * Tessellated glyph outlines, in em space.
43
+ *
44
+ * This is the half of the outline text tier that is expensive: parsing `d`
45
+ * into a path and running earcut over it costs far more than the per-instance
46
+ * transform that follows. It is also the half that does not vary — em space
47
+ * is unit-scale and baseline-relative, so the same glyph of the same face is
48
+ * the same triangles at every size, position and zoom. Tessellate once, keep
49
+ * it, and pay only a scale-and-translate per occurrence.
50
+ *
51
+ * That is why the renderer transforms cached vertices on the CPU and appends
52
+ * them into one shared buffer rather than giving each glyph its own model
53
+ * matrix: a per-glyph matrix would mean a draw call per glyph, throwing away
54
+ * the batching the atlas tier already has, in exchange for saving an
55
+ * arithmetic pass over a few hundred vertices.
56
+ *
57
+ * ### Flatten tolerance
58
+ *
59
+ * Tessellating once means committing to a curve tolerance once, and em space
60
+ * makes that a clean tradeoff rather than a guess: an error of `T` em shows
61
+ * up as `T × fontSize × viewScale` screen pixels. At 1/4096 em, a glyph would
62
+ * have to reach ~4000 screen pixels tall before the flattening error covered
63
+ * a single pixel — well past any size a document is read at, and far enough
64
+ * past the tier's own threshold that the two never interact.
65
+ *
66
+ * ### Bound
67
+ *
68
+ * A plain `Map` with a cap and wholesale eviction. Glyph tessellations are
69
+ * small (a few hundred vertices) and the working set is a charset, not a
70
+ * document — the cap exists so a page that cycles through many faces cannot
71
+ * grow without limit, not because pressure is expected. Wholesale rather than
72
+ * LRU because the refill cost is bounded by what is actually on screen, and
73
+ * an LRU's bookkeeping would cost more than the misses it avoids.
74
+ */
75
+
76
+ /** Test helper. Do not call from product code. */
77
+ declare function _resetOutlineMeshCacheForTests(): void;
78
+
79
+ /**
80
+ * Tessellated glyph *outlines' strokes*, in em space.
81
+ *
82
+ * The sibling of `outlineMeshCache`, and cached for the same reason: parsing
83
+ * `d` and tessellating a ribbon costs far more than the per-instance
84
+ * transform that follows, and em space is size- and position-independent, so
85
+ * one tessellation serves every occurrence of that glyph.
86
+ *
87
+ * ### Why the width is in the key
88
+ *
89
+ * A fill mesh is the same triangles at every size. A stroke's is not — the
90
+ * ribbon's width relative to the glyph decides the geometry, so the em-space
91
+ * mesh depends on `strokeWidth / fontSize`. That ratio is a property of the
92
+ * run's style, not of the frame, so it is stable across frames and makes a
93
+ * perfectly good cache key; what it is *not* is free, so a document that
94
+ * animates stroke width or font size will miss every frame. That is the same
95
+ * bargain `tessellate` makes everywhere else, and it is the reason the width
96
+ * is quantized before it reaches the key: neighbouring sub-quantum widths
97
+ * would otherwise each get their own entry and evict each other.
98
+ *
99
+ * Joins, caps, miter limit, alignment and dash all change the ribbon too, so
100
+ * they are in the key as well — assembled by the caller, which already has
101
+ * the `Stroke` in hand.
102
+ */
103
+
104
+ /** Test helper. Do not call from product code. */
105
+ declare function _resetOutlineStrokeMeshCacheForTests(): void;
106
+
107
+ /**
108
+ * Tessellated stroke ribbons for scene paths, keyed on `Path` identity.
109
+ *
110
+ * The sibling of `cache.ts` (fills) and `outlineStrokeMeshCache.ts` (glyph
111
+ * ribbons in em space), and it inherits their contract: identity, not content.
112
+ * A `Path` rebuilt with equal coords is a distinct entry.
113
+ *
114
+ * Design: `docs/superpowers/specs/2026-08-15-stroke-ribbon-cache-design.md`.
115
+ */
116
+
117
+ /** Test helper. Do not call from product code. */
118
+ declare function _resetStrokeMeshCacheForTests(): void;
119
+
120
+ export { _resetCacheForTests, _resetMarkerCommandCacheForTests, _resetOutlineMeshCacheForTests, _resetOutlineStrokeMeshCacheForTests, _resetStrokeMeshCacheForTests, getMesh };
@@ -0,0 +1,4 @@
1
+ export { FLOATS_PER_VERTEX, __setImageLoaderForTests, _resetCacheForTests, _resetImageCacheForTests, _resetMarkerCommandCacheForTests, _resetOutlineMeshCacheForTests, _resetOutlineStrokeMeshCacheForTests, _resetPaintDrawKindsForTests, _resetPatternSpecCacheForTests, _resetProgramRegistryForTests, _resetStrokeMeshCacheForTests, getMesh } from './chunk-VHQJ56XQ.js';
2
+ export { _resetTextureRegistryForTests } from './chunk-ORBXHIRB.js';
3
+ //# sourceMappingURL=test-seams.js.map
4
+ //# sourceMappingURL=test-seams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"test-seams.js"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Proxy-based recording WebGL2 context for unit tests.
3
+ *
4
+ * Records every method call as { name, args, result } so tests can assert
5
+ * against the call sequence. Returns synthetic handles for object creation
6
+ * (createShader/Program/Buffer/etc.); reports truthy for *Parameter status
7
+ * queries (i.e. assumes shader/program compilation succeeded).
8
+ *
9
+ * NOT a renderer correctness check — pixel-level correctness is verified
10
+ * end-to-end by the smoke test in dev/smoke.spec.ts. This recorder catches
11
+ * "did the dispatcher emit the right call sequence" bugs only.
12
+ */
13
+ interface GLCall {
14
+ readonly name: string;
15
+ readonly args: readonly unknown[];
16
+ readonly result: unknown;
17
+ }
18
+ /** What the recorded context reports about itself. */
19
+ interface GLRecorderOptions {
20
+ /** Reported as `gl.drawingBufferWidth`. Default 800. */
21
+ drawingBufferWidth?: number;
22
+ /** Reported as `gl.drawingBufferHeight`. Default 600. */
23
+ drawingBufferHeight?: number;
24
+ /** Returned by `gl.getContextAttributes()`. Default `{ stencil: true }`. */
25
+ contextAttributes?: Partial<WebGLContextAttributes>;
26
+ }
27
+ /** A stand-in context and the calls made on it, oldest first. */
28
+ interface GLRecorder {
29
+ readonly gl: WebGL2RenderingContext;
30
+ readonly calls: GLCall[];
31
+ reset(): void;
32
+ }
33
+ /** A WebGL2 stand-in that records every call and draws nothing. */
34
+ declare function makeGLRecorder(options?: GLRecorderOptions): GLRecorder;
35
+
36
+ export { type GLCall, type GLRecorder, type GLRecorderOptions, makeGLRecorder };
@@ -0,0 +1,155 @@
1
+ // src/testing/glRecorder.ts
2
+ var GL_CONSTANTS = {
3
+ // Shader / program
4
+ VERTEX_SHADER: 35633,
5
+ FRAGMENT_SHADER: 35632,
6
+ COMPILE_STATUS: 35713,
7
+ LINK_STATUS: 35714,
8
+ // Buffer targets
9
+ ARRAY_BUFFER: 34962,
10
+ ELEMENT_ARRAY_BUFFER: 34963,
11
+ STATIC_DRAW: 35044,
12
+ // Types
13
+ FLOAT: 5126,
14
+ UNSIGNED_INT: 5125,
15
+ // Primitives
16
+ TRIANGLES: 4,
17
+ // Errors / state
18
+ NO_ERROR: 0,
19
+ DEPTH_TEST: 2929,
20
+ BLEND: 3042,
21
+ STENCIL_TEST: 2960,
22
+ CULL_FACE: 2884,
23
+ UNPACK_FLIP_Y_WEBGL: 37440,
24
+ SCISSOR_TEST: 3089,
25
+ MAX_TEXTURE_SIZE: 3379,
26
+ // Blend factors
27
+ SRC_ALPHA: 770,
28
+ ONE_MINUS_SRC_ALPHA: 771,
29
+ ZERO: 0,
30
+ ONE: 1,
31
+ // Buffer bits
32
+ COLOR_BUFFER_BIT: 16384,
33
+ STENCIL_BUFFER_BIT: 1024,
34
+ DEPTH_BUFFER_BIT: 256,
35
+ // Stencil ops
36
+ KEEP: 7680,
37
+ REPLACE: 7681,
38
+ INVERT: 5386,
39
+ // Compare funcs
40
+ ALWAYS: 519,
41
+ NEVER: 512,
42
+ EQUAL: 514,
43
+ NOTEQUAL: 517,
44
+ // Color masks
45
+ FRONT_AND_BACK: 1032,
46
+ // Misc constants used in step 1
47
+ COLOR_WRITEMASK: 3107,
48
+ // Textures (step 3)
49
+ TEXTURE_2D: 3553,
50
+ TEXTURE0: 33984,
51
+ RGBA: 6408,
52
+ UNSIGNED_BYTE: 5121,
53
+ TEXTURE_MIN_FILTER: 10241,
54
+ TEXTURE_MAG_FILTER: 10240,
55
+ LINEAR: 9729,
56
+ NEAREST: 9728,
57
+ TEXTURE_WRAP_S: 10242,
58
+ TEXTURE_WRAP_T: 10243,
59
+ CLAMP_TO_EDGE: 33071,
60
+ // Wrap modes (step 4)
61
+ REPEAT: 10497,
62
+ MIRRORED_REPEAT: 33648,
63
+ // Mipmap minification filter
64
+ LINEAR_MIPMAP_LINEAR: 9987,
65
+ // Framebuffers and renderbuffers (offscreen effect passes). Every one of
66
+ // these has to be here before a test names it: an absent ALL-CAPS property
67
+ // reads as 0 through the proxy, so `expect(args[0]).toBe(gl.FRAMEBUFFER)`
68
+ // would compare 0 against 0 and pass over a renderer that bound nothing.
69
+ FRAMEBUFFER: 36160,
70
+ RENDERBUFFER: 36161,
71
+ FRAMEBUFFER_BINDING: 36006,
72
+ RENDERBUFFER_BINDING: 36007,
73
+ COLOR_ATTACHMENT0: 36064,
74
+ DEPTH_STENCIL_ATTACHMENT: 33306,
75
+ DEPTH24_STENCIL8: 35056,
76
+ RGBA8: 32856,
77
+ FRAMEBUFFER_COMPLETE: 36053
78
+ };
79
+ function syntheticHandle(name, seq) {
80
+ return { __id: `${name}_${seq}` };
81
+ }
82
+ function snapshot(value) {
83
+ return ArrayBuffer.isView(value) && !(value instanceof DataView) ? value.slice() : value;
84
+ }
85
+ function makeGLRecorder(options = {}) {
86
+ const calls = [];
87
+ const {
88
+ drawingBufferWidth = 800,
89
+ drawingBufferHeight = 600,
90
+ contextAttributes = { stencil: true }
91
+ } = options;
92
+ const DATA_PROPERTIES = {
93
+ drawingBufferWidth,
94
+ drawingBufferHeight
95
+ };
96
+ const handler = (name) => (...args) => {
97
+ let result = void 0;
98
+ switch (name) {
99
+ case "createShader":
100
+ case "createProgram":
101
+ case "createBuffer":
102
+ case "createVertexArray":
103
+ case "createTexture":
104
+ case "createFramebuffer":
105
+ case "createRenderbuffer":
106
+ result = syntheticHandle(name, calls.length);
107
+ break;
108
+ case "getUniformLocation":
109
+ case "getAttribLocation":
110
+ result = calls.length;
111
+ break;
112
+ case "getShaderParameter":
113
+ case "getProgramParameter":
114
+ result = true;
115
+ break;
116
+ case "getShaderInfoLog":
117
+ case "getProgramInfoLog":
118
+ result = "";
119
+ break;
120
+ case "getContextAttributes":
121
+ result = { ...contextAttributes };
122
+ break;
123
+ case "getError":
124
+ result = 0;
125
+ break;
126
+ default:
127
+ result = void 0;
128
+ }
129
+ calls.push({ name, args: args.map(snapshot), result });
130
+ return result;
131
+ };
132
+ const gl = new Proxy(
133
+ {},
134
+ {
135
+ get(_, prop) {
136
+ if (typeof prop !== "string") return void 0;
137
+ if (prop in DATA_PROPERTIES) return DATA_PROPERTIES[prop];
138
+ if (prop in GL_CONSTANTS) return GL_CONSTANTS[prop];
139
+ if (/^[A-Z_0-9]+$/.test(prop)) return 0;
140
+ return handler(prop);
141
+ }
142
+ }
143
+ );
144
+ return {
145
+ gl,
146
+ calls,
147
+ reset() {
148
+ calls.length = 0;
149
+ }
150
+ };
151
+ }
152
+
153
+ export { makeGLRecorder };
154
+ //# sourceMappingURL=glRecorder.js.map
155
+ //# sourceMappingURL=glRecorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/testing/glRecorder.ts"],"names":[],"mappings":"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","file":"glRecorder.js","sourcesContent":["/**\n * Proxy-based recording WebGL2 context for unit tests.\n *\n * Records every method call as { name, args, result } so tests can assert\n * against the call sequence. Returns synthetic handles for object creation\n * (createShader/Program/Buffer/etc.); reports truthy for *Parameter status\n * queries (i.e. assumes shader/program compilation succeeded).\n *\n * NOT a renderer correctness check — pixel-level correctness is verified\n * end-to-end by the smoke test in dev/smoke.spec.ts. This recorder catches\n * \"did the dispatcher emit the right call sequence\" bugs only.\n */\n\nexport interface GLCall {\n readonly name: string;\n readonly args: readonly unknown[];\n readonly result: unknown;\n}\n\n/** What the recorded context reports about itself. */\nexport interface GLRecorderOptions {\n /** Reported as `gl.drawingBufferWidth`. Default 800. */\n drawingBufferWidth?: number;\n /** Reported as `gl.drawingBufferHeight`. Default 600. */\n drawingBufferHeight?: number;\n /** Returned by `gl.getContextAttributes()`. Default `{ stencil: true }`. */\n contextAttributes?: Partial<WebGLContextAttributes>;\n}\n\n/** A stand-in context and the calls made on it, oldest first. */\nexport interface GLRecorder {\n readonly gl: WebGL2RenderingContext;\n readonly calls: GLCall[];\n reset(): void;\n}\n\nconst GL_CONSTANTS: Readonly<Record<string, number>> = {\n // Shader / program\n VERTEX_SHADER: 0x8B31,\n FRAGMENT_SHADER: 0x8B30,\n COMPILE_STATUS: 0x8B81,\n LINK_STATUS: 0x8B82,\n // Buffer targets\n ARRAY_BUFFER: 0x8892,\n ELEMENT_ARRAY_BUFFER: 0x8893,\n STATIC_DRAW: 0x88E4,\n // Types\n FLOAT: 0x1406,\n UNSIGNED_INT: 0x1405,\n // Primitives\n TRIANGLES: 0x0004,\n // Errors / state\n NO_ERROR: 0,\n DEPTH_TEST: 0x0B71,\n BLEND: 0x0BE2,\n STENCIL_TEST: 0x0B90,\n CULL_FACE: 0x0B44,\n UNPACK_FLIP_Y_WEBGL: 0x9240,\n SCISSOR_TEST: 0x0C11,\n MAX_TEXTURE_SIZE: 0x0D33,\n // Blend factors\n SRC_ALPHA: 0x0302,\n ONE_MINUS_SRC_ALPHA: 0x0303,\n ZERO: 0,\n ONE: 1,\n // Buffer bits\n COLOR_BUFFER_BIT: 0x00004000,\n STENCIL_BUFFER_BIT: 0x00000400,\n DEPTH_BUFFER_BIT: 0x00000100,\n // Stencil ops\n KEEP: 0x1E00,\n REPLACE: 0x1E01,\n INVERT: 0x150A,\n // Compare funcs\n ALWAYS: 0x0207,\n NEVER: 0x0200,\n EQUAL: 0x0202,\n NOTEQUAL: 0x0205,\n // Color masks\n FRONT_AND_BACK: 0x0408,\n // Misc constants used in step 1\n COLOR_WRITEMASK: 0x0C23,\n // Textures (step 3)\n TEXTURE_2D: 0x0DE1,\n TEXTURE0: 0x84C0,\n RGBA: 0x1908,\n UNSIGNED_BYTE: 0x1401,\n TEXTURE_MIN_FILTER: 0x2801,\n TEXTURE_MAG_FILTER: 0x2800,\n LINEAR: 0x2601,\n NEAREST: 0x2600,\n TEXTURE_WRAP_S: 0x2802,\n TEXTURE_WRAP_T: 0x2803,\n CLAMP_TO_EDGE: 0x812F,\n // Wrap modes (step 4)\n REPEAT: 0x2901,\n MIRRORED_REPEAT: 0x8370,\n // Mipmap minification filter\n LINEAR_MIPMAP_LINEAR: 0x2703,\n // Framebuffers and renderbuffers (offscreen effect passes). Every one of\n // these has to be here before a test names it: an absent ALL-CAPS property\n // reads as 0 through the proxy, so `expect(args[0]).toBe(gl.FRAMEBUFFER)`\n // would compare 0 against 0 and pass over a renderer that bound nothing.\n FRAMEBUFFER: 0x8D40,\n RENDERBUFFER: 0x8D41,\n FRAMEBUFFER_BINDING: 0x8CA6,\n RENDERBUFFER_BINDING: 0x8CA7,\n COLOR_ATTACHMENT0: 0x8CE0,\n DEPTH_STENCIL_ATTACHMENT: 0x821A,\n DEPTH24_STENCIL8: 0x88F0,\n RGBA8: 0x8058,\n FRAMEBUFFER_COMPLETE: 0x8CD5,\n};\n\nfunction syntheticHandle(name: string, seq: number): { __id: string } {\n return { __id: `${name}_${seq}` };\n}\n\n/**\n * Copy typed-array arguments as they are recorded.\n *\n * A caller is entitled to reuse the array it uploads from — the text ring\n * packs every draw through one scratch buffer — so storing the reference\n * records a value that later frames overwrite. A test reading it back then\n * compares two frames and sees the same numbers twice, which passes.\n */\nfunction snapshot(value: unknown): unknown {\n return ArrayBuffer.isView(value) && !(value instanceof DataView)\n ? (value as unknown as { slice(): unknown }).slice()\n : value;\n}\n\n/** A WebGL2 stand-in that records every call and draws nothing. */\nexport function makeGLRecorder(options: GLRecorderOptions = {}): GLRecorder {\n const calls: GLCall[] = [];\n const {\n drawingBufferWidth = 800,\n drawingBufferHeight = 600,\n contextAttributes = { stencil: true },\n } = options;\n\n // Read as data, not called. The Proxy below hands back a recording function\n // for every lowercase property, so without this `drawingBufferHeight` is a\n // function and every y-flip computed from it is NaN — silently.\n const DATA_PROPERTIES: Readonly<Record<string, unknown>> = {\n drawingBufferWidth,\n drawingBufferHeight,\n };\n\n const handler = (name: string) => (...args: unknown[]) => {\n let result: unknown = undefined;\n switch (name) {\n case 'createShader':\n case 'createProgram':\n case 'createBuffer':\n case 'createVertexArray':\n case 'createTexture':\n case 'createFramebuffer':\n case 'createRenderbuffer':\n result = syntheticHandle(name, calls.length);\n break;\n case 'getUniformLocation':\n case 'getAttribLocation':\n result = calls.length;\n break;\n case 'getShaderParameter':\n case 'getProgramParameter':\n result = true;\n break;\n case 'getShaderInfoLog':\n case 'getProgramInfoLog':\n result = '';\n break;\n case 'getContextAttributes':\n result = { ...contextAttributes };\n break;\n case 'getError':\n result = 0;\n break;\n default:\n result = undefined;\n }\n calls.push({ name, args: args.map(snapshot), result });\n return result;\n };\n\n const gl = new Proxy(\n {},\n {\n get(_, prop: string | symbol) {\n if (typeof prop !== 'string') return undefined;\n if (prop in DATA_PROPERTIES) return DATA_PROPERTIES[prop];\n if (prop in GL_CONSTANTS) return GL_CONSTANTS[prop];\n if (/^[A-Z_0-9]+$/.test(prop)) return 0; // unknown all-caps constant → 0\n return handler(prop);\n },\n },\n ) as unknown as WebGL2RenderingContext;\n\n return {\n gl,\n calls,\n reset() {\n calls.length = 0;\n },\n };\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import { GLRecorder } from './glRecorder.js';
2
+
3
+ /**
4
+ * Read vertex geometry back out of a `GLRecorder`, with each upload's
5
+ * floats-per-vertex taken from the `vertexAttribPointer` recorded for that same
6
+ * buffer object.
7
+ *
8
+ * Pairing an upload with the pointer call next to it used to work because every
9
+ * draw minted its own VAO and buffers. The text paths now take a slot from a
10
+ * ring that configured its attributes once, so a frame after the first records
11
+ * no pointer calls at all — hence a tracker that remembers what it has learned
12
+ * rather than a function over one frame's calls. Ask it for `uploads()` after
13
+ * each frame and it carries the strides across `recorder.reset()`.
14
+ */
15
+
16
+ /** One vertex buffer upload. */
17
+ interface VertexUpload {
18
+ /** Floats per vertex. */
19
+ stride: number;
20
+ data: Float32Array;
21
+ }
22
+ /** Reads vertex uploads out of a recorder, remembering each buffer's stride. */
23
+ interface UploadTracker {
24
+ /** Every vertex upload recorded since the last `recorder.reset()`. */
25
+ uploads(): VertexUpload[];
26
+ }
27
+ /** A tracker over `recorder`; keep one across frames. */
28
+ declare function makeUploadTracker(recorder: GLRecorder): UploadTracker;
29
+
30
+ export { type UploadTracker, type VertexUpload, makeUploadTracker };
@@ -0,0 +1,48 @@
1
+ // src/testing/vertexUploads.ts
2
+ var ARRAY_BUFFER = 34962;
3
+ function payload(call) {
4
+ const data = call.name === "bufferData" ? call.args[1] : call.args[2];
5
+ if (!(data instanceof Float32Array)) return null;
6
+ if (call.name !== "bufferSubData") return data;
7
+ const srcOffset = typeof call.args[3] === "number" ? call.args[3] : 0;
8
+ const length = typeof call.args[4] === "number" ? call.args[4] : data.length - srcOffset;
9
+ return data.subarray(srcOffset, srcOffset + length);
10
+ }
11
+ function makeUploadTracker(recorder) {
12
+ const strides = /* @__PURE__ */ new Map();
13
+ return {
14
+ uploads() {
15
+ const out = [];
16
+ const pending = [];
17
+ let bound = null;
18
+ for (const call of recorder.calls) {
19
+ if (call.name === "bindBuffer") {
20
+ if (call.args[0] === ARRAY_BUFFER) bound = call.args[1];
21
+ continue;
22
+ }
23
+ if (call.name === "vertexAttribPointer") {
24
+ const bytes = call.args[4];
25
+ strides.set(bound, bytes === 0 ? 2 : bytes / 4);
26
+ continue;
27
+ }
28
+ if (call.name !== "bufferData" && call.name !== "bufferSubData") continue;
29
+ const data = payload(call);
30
+ if (data) pending.push({ buffer: bound, data });
31
+ }
32
+ for (const { buffer, data } of pending) {
33
+ const stride = strides.get(buffer);
34
+ if (stride === void 0) {
35
+ throw new Error(
36
+ "vertexUploads: a vertex buffer was written with no vertexAttribPointer ever recorded for it \u2014 call uploads() on the frame that creates the buffer, before resetting the recorder."
37
+ );
38
+ }
39
+ out.push({ stride, data });
40
+ }
41
+ return out;
42
+ }
43
+ };
44
+ }
45
+
46
+ export { makeUploadTracker };
47
+ //# sourceMappingURL=vertexUploads.js.map
48
+ //# sourceMappingURL=vertexUploads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/testing/vertexUploads.ts"],"names":[],"mappings":";AAeA,IAAM,YAAA,GAAe,KAAA;AAwBrB,SAAS,QAAQ,IAAA,EAAmC;AAClD,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,KAAS,YAAA,GAAe,IAAA,CAAK,KAAK,CAAC,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA;AACpE,EAAA,IAAI,EAAE,IAAA,YAAgB,YAAA,CAAA,EAAe,OAAO,IAAA;AAC5C,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,eAAA,EAAiB,OAAO,IAAA;AAC1C,EAAA,MAAM,SAAA,GAAY,OAAO,IAAA,CAAK,IAAA,CAAK,CAAC,MAAM,QAAA,GAAW,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,GAAI,CAAA;AACpE,EAAA,MAAM,MAAA,GAAS,OAAO,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,QAAA,GAAW,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,GAAI,IAAA,CAAK,MAAA,GAAS,SAAA;AAC/E,EAAA,OAAO,IAAA,CAAK,QAAA,CAAS,SAAA,EAAW,SAAA,GAAY,MAAM,CAAA;AACpD;AAGO,SAAS,kBAAkB,QAAA,EAAqC;AAErE,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAqB;AAEzC,EAAA,OAAO;AAAA,IACL,OAAA,GAA0B;AACxB,MAAA,MAAM,MAAsB,EAAC;AAC7B,MAAA,MAAM,UAAqD,EAAC;AAC5D,MAAA,IAAI,KAAA,GAAiB,IAAA;AAIrB,MAAA,KAAA,MAAW,IAAA,IAAQ,SAAS,KAAA,EAAO;AACjC,QAAA,IAAI,IAAA,CAAK,SAAS,YAAA,EAAc;AAC9B,UAAA,IAAI,IAAA,CAAK,KAAK,CAAC,CAAA,KAAM,cAAc,KAAA,GAAQ,IAAA,CAAK,KAAK,CAAC,CAAA;AACtD,UAAA;AAAA,QACF;AACA,QAAA,IAAI,IAAA,CAAK,SAAS,qBAAA,EAAuB;AAEvC,UAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA;AACzB,UAAA,OAAA,CAAQ,IAAI,KAAA,EAAO,KAAA,KAAU,CAAA,GAAI,CAAA,GAAI,QAAQ,CAAC,CAAA;AAC9C,UAAA;AAAA,QACF;AACA,QAAA,IAAI,IAAA,CAAK,IAAA,KAAS,YAAA,IAAgB,IAAA,CAAK,SAAS,eAAA,EAAiB;AACjE,QAAA,MAAM,IAAA,GAAO,QAAQ,IAAI,CAAA;AACzB,QAAA,IAAI,MAAM,OAAA,CAAQ,IAAA,CAAK,EAAE,MAAA,EAAQ,KAAA,EAAO,MAAM,CAAA;AAAA,MAChD;AAEA,MAAA,KAAA,MAAW,EAAE,MAAA,EAAQ,IAAA,EAAK,IAAK,OAAA,EAAS;AACtC,QAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,GAAA,CAAI,MAAM,CAAA;AACjC,QAAA,IAAI,WAAW,MAAA,EAAW;AACxB,UAAA,MAAM,IAAI,KAAA;AAAA,YACR;AAAA,WAGF;AAAA,QACF;AACA,QAAA,GAAA,CAAI,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,MAC3B;AACA,MAAA,OAAO,GAAA;AAAA,IACT;AAAA,GACF;AACF","file":"vertexUploads.js","sourcesContent":["/**\n * Read vertex geometry back out of a `GLRecorder`, with each upload's\n * floats-per-vertex taken from the `vertexAttribPointer` recorded for that same\n * buffer object.\n *\n * Pairing an upload with the pointer call next to it used to work because every\n * draw minted its own VAO and buffers. The text paths now take a slot from a\n * ring that configured its attributes once, so a frame after the first records\n * no pointer calls at all — hence a tracker that remembers what it has learned\n * rather than a function over one frame's calls. Ask it for `uploads()` after\n * each frame and it carries the strides across `recorder.reset()`.\n */\n\nimport type { GLCall, GLRecorder } from './glRecorder';\n\nconst ARRAY_BUFFER = 0x8892;\n\n/** One vertex buffer upload. */\nexport interface VertexUpload {\n /** Floats per vertex. */\n stride: number;\n data: Float32Array;\n}\n\n/** Reads vertex uploads out of a recorder, remembering each buffer's stride. */\nexport interface UploadTracker {\n /** Every vertex upload recorded since the last `recorder.reset()`. */\n uploads(): VertexUpload[];\n}\n\n/**\n * The floats an upload actually sent.\n *\n * `bufferSubData(target, offset, src, srcOffset, length)` hands GL a *window*\n * into a scratch array the batch reuses across frames, so the recorded\n * argument is the whole scratch — the tail of it being whatever the last, and\n * possibly longer, run left there. Reading it whole compares live vertices\n * against stale ones.\n */\nfunction payload(call: GLCall): Float32Array | null {\n const data = call.name === 'bufferData' ? call.args[1] : call.args[2];\n if (!(data instanceof Float32Array)) return null;\n if (call.name !== 'bufferSubData') return data;\n const srcOffset = typeof call.args[3] === 'number' ? call.args[3] : 0;\n const length = typeof call.args[4] === 'number' ? call.args[4] : data.length - srcOffset;\n return data.subarray(srcOffset, srcOffset + length);\n}\n\n/** A tracker over `recorder`; keep one across frames. */\nexport function makeUploadTracker(recorder: GLRecorder): UploadTracker {\n // Keyed on the recorder's synthetic buffer handle, which is a stable object.\n const strides = new Map<unknown, number>();\n\n return {\n uploads(): VertexUpload[] {\n const out: VertexUpload[] = [];\n const pending: { buffer: unknown; data: Float32Array }[] = [];\n let bound: unknown = null;\n\n // Two passes: a buffer's attribute layout can be recorded either before\n // its first upload (the ring) or after it (every other draw path).\n for (const call of recorder.calls) {\n if (call.name === 'bindBuffer') {\n if (call.args[0] === ARRAY_BUFFER) bound = call.args[1];\n continue;\n }\n if (call.name === 'vertexAttribPointer') {\n // Byte stride; 0 means tightly packed, which for a_position is a vec2.\n const bytes = call.args[4] as number;\n strides.set(bound, bytes === 0 ? 2 : bytes / 4);\n continue;\n }\n if (call.name !== 'bufferData' && call.name !== 'bufferSubData') continue;\n const data = payload(call);\n if (data) pending.push({ buffer: bound, data });\n }\n\n for (const { buffer, data } of pending) {\n const stride = strides.get(buffer);\n if (stride === undefined) {\n throw new Error(\n 'vertexUploads: a vertex buffer was written with no vertexAttribPointer ' +\n 'ever recorded for it — call uploads() on the frame that creates the ' +\n 'buffer, before resetting the recorder.',\n );\n }\n out.push({ stride, data });\n }\n return out;\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,73 @@
1
1
  {
2
2
  "name": "@weasel-js/renderer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.9.4",
4
+ "description": "A WebGL2 renderer for draw commands: batched path fills and strokes, MSDF and outline text, images and sprites, gradients, patterns, registered shader programs, and full-screen effects. No scene graph, no React.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "./mesh": {
17
+ "import": "./dist/mesh.js",
18
+ "types": "./dist/mesh.d.ts"
19
+ },
20
+ "./patterns-builtin": {
21
+ "import": "./dist/patterns-builtin.js",
22
+ "types": "./dist/patterns-builtin.d.ts"
23
+ },
24
+ "./testing/glRecorder": {
25
+ "import": "./dist/testing/glRecorder.js",
26
+ "types": "./dist/testing/glRecorder.d.ts"
27
+ },
28
+ "./testing/vertexUploads": {
29
+ "import": "./dist/testing/vertexUploads.js",
30
+ "types": "./dist/testing/vertexUploads.d.ts"
31
+ },
32
+ "./test-seams": {
33
+ "import": "./dist/test-seams.js",
34
+ "types": "./dist/test-seams.d.ts"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "dependencies": {
39
+ "@weasel-js/geom": "1.9.4",
40
+ "@weasel-js/paint": "1.9.4",
41
+ "@weasel-js/registry": "1.9.4",
42
+ "@weasel-js/stroke": "1.9.4",
43
+ "@weasel-js/text": "1.9.4"
44
+ },
45
+ "peerDependencies": {
46
+ "@weasel-js/font": "1.9.4"
47
+ },
48
+ "author": "orochi235",
49
+ "homepage": "https://orochi235.github.io/weasel/",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/orochi235/weasel.git",
53
+ "directory": "packages/renderer"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/orochi235/weasel/issues"
57
+ },
58
+ "engines": {
59
+ "node": ">=22"
60
+ },
61
+ "files": [
62
+ "dist",
63
+ "README.md",
64
+ "LICENSE"
65
+ ],
66
+ "scripts": {
67
+ "build": "tsup"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public",
71
+ "provenance": true
72
+ }
73
+ }