@weasel-js/labkit 1.0.0 → 1.0.1

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.
@@ -400,32 +400,53 @@ function affordanceKindOf(target) {
400
400
  const k = target.kind;
401
401
  return typeof k === "string" ? k : void 0;
402
402
  }
403
- function matchTarget(target, specTarget, bodyTarget, bodyKind) {
404
- if (specTarget === void 0) return true;
405
- if (typeof specTarget === "object" && specTarget !== null && "kindOf" in specTarget && typeof specTarget.kindOf === "function") {
406
- return specTarget.kindOf(target, bodyTarget);
403
+ function hasPrefix(s, prefix) {
404
+ return s.startsWith(prefix);
405
+ }
406
+ function parseTargetSpec(spec) {
407
+ if (typeof spec === "object") {
408
+ return spec !== null && typeof spec.kindOf === "function" ? { form: "predicate", kindOf: spec.kindOf } : null;
407
409
  }
408
- if (typeof specTarget !== "string") return false;
409
- if (specTarget === "empty" || specTarget === "selected-body" || specTarget === "unselected-body") {
410
- if (bodyTarget === void 0) return false;
411
- return bodyTarget === specTarget;
410
+ if (typeof spec !== "string") return null;
411
+ if (spec === "empty" || spec === "selected-body" || spec === "unselected-body") {
412
+ return { form: "body", body: spec };
412
413
  }
413
- if (specTarget.startsWith("affordance:")) {
414
- const wanted = specTarget.slice("affordance:".length);
415
- return affordanceKindOf(target) === wanted;
414
+ if (hasPrefix(spec, "affordance:")) {
415
+ return { form: "affordance", kind: spec.slice("affordance:".length) };
416
+ }
417
+ if (hasPrefix(spec, "kind:")) {
418
+ const rest = spec.slice("kind:".length);
419
+ const requireSelected = rest.endsWith(":selected");
420
+ return {
421
+ form: "kind",
422
+ kind: requireSelected ? rest.slice(0, -":selected".length) : rest,
423
+ requireSelected
424
+ };
416
425
  }
417
- if (specTarget.startsWith("kind:")) {
418
- if (bodyKind === void 0) return false;
419
- let wanted = specTarget.slice("kind:".length);
420
- let requireSelected = false;
421
- if (wanted.endsWith(":selected")) {
422
- wanted = wanted.slice(0, -":selected".length);
423
- requireSelected = true;
426
+ return null;
427
+ }
428
+ function matchTarget(target, specTarget, bodyTarget, bodyKind) {
429
+ if (specTarget === void 0) return true;
430
+ const form = parseTargetSpec(specTarget);
431
+ if (form === null) return false;
432
+ switch (form.form) {
433
+ // Receives the raw target (affordance hit for drag, e.target otherwise)
434
+ // plus the bodyTarget classification.
435
+ case "predicate":
436
+ return form.kindOf(target, bodyTarget);
437
+ // Resolved from the `classifyTarget` result packed into the event's
438
+ // `bodyTarget`. Unwired classifyTarget → absent → no match.
439
+ case "body":
440
+ return bodyTarget !== void 0 && bodyTarget === form.body;
441
+ case "affordance":
442
+ return affordanceKindOf(target) === form.kind;
443
+ case "kind":
444
+ if (bodyKind === void 0 || bodyKind !== form.kind) return false;
445
+ return form.requireSelected ? bodyTarget === "selected-body" : true;
446
+ default: {
447
+ return false;
424
448
  }
425
- if (bodyKind !== wanted) return false;
426
- return requireSelected ? bodyTarget === "selected-body" : true;
427
449
  }
428
- return false;
429
450
  }
430
451
  function normalizePhase(spec) {
431
452
  if (typeof spec === "string") {
@@ -3456,7 +3477,7 @@ function serialToEntry(se, custom, logger) {
3456
3477
  };
3457
3478
  }
3458
3479
 
3459
- // ../core/dist/chunk-D2C6K6FO.js
3480
+ // ../core/dist/chunk-FVNWK7U3.js
3460
3481
  function worldToScreen(worldX, worldY, view) {
3461
3482
  return [view.panX + worldX * view.zoom.x, view.panY + worldY * view.zoom.y];
3462
3483
  }
@@ -5548,7 +5569,7 @@ function warnMissingGlyphOnce(family, cp) {
5548
5569
  `weasel layoutRuns: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} (${JSON.stringify(ch)}) in "${family}", and the dynamic tier could not rasterize it \u2014 skipping the character. Bake it into the atlas, or call registerCanvasFont("${family}") to serve missing codepoints from installed fonts.`
5549
5570
  );
5550
5571
  }
5551
- function layoutRuns(runs, opts, origin) {
5572
+ function layoutRuns(runs, opts) {
5552
5573
  const ctx = { groups: /* @__PURE__ */ new Map() };
5553
5574
  const entries = [];
5554
5575
  let prevCp;
@@ -5718,7 +5739,7 @@ function layoutRuns(runs, opts, origin) {
5718
5739
  }
5719
5740
  }
5720
5741
  const lineBoxes = [];
5721
- let penY = origin.y;
5742
+ let penY = 0;
5722
5743
  let maxLineWidth = 0;
5723
5744
  const finiteWidth = Number.isFinite(opts.maxWidth) ? opts.maxWidth : 0;
5724
5745
  for (const line of lines) {
@@ -5730,7 +5751,7 @@ function layoutRuns(runs, opts, origin) {
5730
5751
  const slack = finiteWidth - line.width;
5731
5752
  return opts.align === "center" ? slack / 2 : slack;
5732
5753
  })();
5733
- const lineX0 = origin.x + alignShift;
5754
+ const lineX0 = alignShift;
5734
5755
  const baselineSource = line.entries[0] ?? line.blank;
5735
5756
  const lineBaselineY = baselineSource ? penY + baselineSource.font.common.base * (baselineSource.fontSize / baselineSource.font.info.size) : penY;
5736
5757
  let penX = lineX0;
@@ -5804,7 +5825,7 @@ function layoutRuns(runs, opts, origin) {
5804
5825
  groups: [...ctx.groups.values()],
5805
5826
  decorations,
5806
5827
  lines: lineBoxes,
5807
- bounds: { width: maxLineWidth, height: penY - origin.y }
5828
+ bounds: { width: maxLineWidth, height: penY }
5808
5829
  };
5809
5830
  }
5810
5831
  var LAYOUT_CACHE_VARIANT_LIMIT = 8;
@@ -5817,10 +5838,10 @@ function outlineBucket(runs, min) {
5817
5838
  for (const size of sizes) if (size >= min) n++;
5818
5839
  return n;
5819
5840
  }
5820
- function variantKey2(runs, opts, origin) {
5821
- return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}|${origin.x}|${origin.y}`;
5841
+ function variantKey2(runs, opts) {
5842
+ return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}`;
5822
5843
  }
5823
- function cachedLayoutRuns(runs, opts, origin) {
5844
+ function cachedLayoutRuns(runs, opts) {
5824
5845
  const fonts = glyphGeneration();
5825
5846
  let entry = cache2.get(runs);
5826
5847
  if (entry === void 0) {
@@ -5830,10 +5851,10 @@ function cachedLayoutRuns(runs, opts, origin) {
5830
5851
  entry.generation = fonts;
5831
5852
  entry.byVariant.clear();
5832
5853
  }
5833
- const key = variantKey2(runs, opts, origin);
5854
+ const key = variantKey2(runs, opts);
5834
5855
  const hit = entry.byVariant.get(key);
5835
5856
  if (hit !== void 0) return hit;
5836
- const laid = layoutRuns(runs, opts, origin);
5857
+ const laid = layoutRuns(runs, opts);
5837
5858
  if (entry.byVariant.size >= LAYOUT_CACHE_VARIANT_LIMIT) entry.byVariant.clear();
5838
5859
  entry.byVariant.set(key, laid);
5839
5860
  return laid;
@@ -6141,25 +6162,264 @@ function outlineStrokeMesh(glyphKey, d, emWidth, stroke) {
6141
6162
  cache4.set(key, mesh);
6142
6163
  return mesh.indices.length === 0 ? null : mesh;
6143
6164
  }
6144
- function setColorMatrixUniforms(ctx, prog) {
6145
- const gl = ctx.gl;
6146
- const cm = ctx.state.colorMatrix;
6147
- const m4 = new Float32Array(16);
6148
- for (let row = 0; row < 4; row++) {
6149
- for (let col = 0; col < 4; col++) {
6150
- m4[col * 4 + row] = cm[row * 5 + col];
6165
+ var MAX_VERTICES_PER_BATCH = 32768;
6166
+ var FLOATS_PER_VERTEX = 6;
6167
+ var INITIAL_VERTICES = 256;
6168
+ var INITIAL_INDICES = 384;
6169
+ var SolidBatch = class {
6170
+ gl;
6171
+ vao;
6172
+ vbo;
6173
+ ibo;
6174
+ verts = new Float32Array(INITIAL_VERTICES * FLOATS_PER_VERTEX);
6175
+ idx = new Uint32Array(INITIAL_INDICES);
6176
+ nVerts = 0;
6177
+ nIdx = 0;
6178
+ /** What the GPU buffers are currently sized for. */
6179
+ vboVerts = 0;
6180
+ iboIdx = 0;
6181
+ constructor(gl, prog) {
6182
+ const aPos = prog.attribute("a_position");
6183
+ const aColor = prog.attribute("a_vertexColor");
6184
+ if (aPos === void 0 || aColor === void 0) {
6185
+ throw new Error("SolidBatch: vertex-color program is missing a_position / a_vertexColor");
6186
+ }
6187
+ const vao = gl.createVertexArray();
6188
+ const vbo = gl.createBuffer();
6189
+ const ibo = gl.createBuffer();
6190
+ if (!vao || !vbo || !ibo) throw new Error("SolidBatch: failed to create GL objects");
6191
+ this.gl = gl;
6192
+ this.vao = vao;
6193
+ this.vbo = vbo;
6194
+ this.ibo = ibo;
6195
+ const stride = FLOATS_PER_VERTEX * 4;
6196
+ gl.bindVertexArray(vao);
6197
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
6198
+ gl.enableVertexAttribArray(aPos);
6199
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, stride, 0);
6200
+ gl.enableVertexAttribArray(aColor);
6201
+ gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 8);
6202
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
6203
+ gl.bindVertexArray(null);
6204
+ this.growGpu(INITIAL_VERTICES, INITIAL_INDICES);
6205
+ }
6206
+ get length() {
6207
+ return this.nIdx;
6208
+ }
6209
+ /** Whether staging `vertices` more would put the run past the per-flush cap. */
6210
+ wouldOverflow(vertices) {
6211
+ return this.nVerts + vertices > MAX_VERTICES_PER_BATCH;
6212
+ }
6213
+ /**
6214
+ * Append one rect's four corners through `m`, all carrying `rgba` (straight
6215
+ * alpha). An affine maps a rect to a parallelogram, so two triangles still
6216
+ * cover it and the batch draws at `u_model` identity.
6217
+ */
6218
+ pushRect(x, y, w, h, m, r, g, b, a) {
6219
+ this.reserve(4, 6);
6220
+ const v = this.verts;
6221
+ let i = this.nVerts * FLOATS_PER_VERTEX;
6222
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
6223
+ const x1 = x + w;
6224
+ const y1 = y + h;
6225
+ const ax = ma * x + mc * y + mtx, ay = mb * x + md * y + mty;
6226
+ const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
6227
+ const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
6228
+ const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
6229
+ v[i++] = ax;
6230
+ v[i++] = ay;
6231
+ v[i++] = r;
6232
+ v[i++] = g;
6233
+ v[i++] = b;
6234
+ v[i++] = a;
6235
+ v[i++] = bx;
6236
+ v[i++] = by;
6237
+ v[i++] = r;
6238
+ v[i++] = g;
6239
+ v[i++] = b;
6240
+ v[i++] = a;
6241
+ v[i++] = cx;
6242
+ v[i++] = cy;
6243
+ v[i++] = r;
6244
+ v[i++] = g;
6245
+ v[i++] = b;
6246
+ v[i++] = a;
6247
+ v[i++] = dx;
6248
+ v[i++] = dy;
6249
+ v[i++] = r;
6250
+ v[i++] = g;
6251
+ v[i++] = b;
6252
+ v[i++] = a;
6253
+ const base = this.nVerts;
6254
+ let j = this.nIdx;
6255
+ this.idx[j++] = base;
6256
+ this.idx[j++] = base + 1;
6257
+ this.idx[j++] = base + 2;
6258
+ this.idx[j++] = base;
6259
+ this.idx[j++] = base + 2;
6260
+ this.idx[j++] = base + 3;
6261
+ this.nVerts += 4;
6262
+ this.nIdx += 6;
6263
+ }
6264
+ /**
6265
+ * Append a tessellated mesh through `m`, all vertices carrying `rgba`. The
6266
+ * mesh's own indices are rebased onto the staged vertices, which is why the
6267
+ * index buffer is uploaded per flush rather than written once.
6268
+ */
6269
+ pushMesh(mesh, m, r, g, b, a) {
6270
+ const src = mesh.vertices;
6271
+ const srcIdx = mesh.indices;
6272
+ const n = src.length >> 1;
6273
+ this.reserve(n, srcIdx.length);
6274
+ const v = this.verts;
6275
+ let i = this.nVerts * FLOATS_PER_VERTEX;
6276
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
6277
+ for (let k = 0; k < n; k++) {
6278
+ const x = src[k * 2];
6279
+ const y = src[k * 2 + 1];
6280
+ v[i++] = ma * x + mc * y + mtx;
6281
+ v[i++] = mb * x + md * y + mty;
6282
+ v[i++] = r;
6283
+ v[i++] = g;
6284
+ v[i++] = b;
6285
+ v[i++] = a;
6286
+ }
6287
+ const base = this.nVerts;
6288
+ const out = this.idx;
6289
+ let j = this.nIdx;
6290
+ for (let k = 0; k < srcIdx.length; k++) out[j++] = base + srcIdx[k];
6291
+ this.nVerts += n;
6292
+ this.nIdx += srcIdx.length;
6293
+ }
6294
+ /** Upload the staged geometry and bind the batch VAO. Returns the index
6295
+ * count for the caller's `drawElements`. */
6296
+ uploadAndBind() {
6297
+ const gl = this.gl;
6298
+ if (this.nVerts > this.vboVerts || this.nIdx > this.iboIdx) {
6299
+ this.growGpu(this.nVerts, this.nIdx);
6151
6300
  }
6301
+ gl.bindVertexArray(this.vao);
6302
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
6303
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nVerts * FLOATS_PER_VERTEX);
6304
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, this.idx, 0, this.nIdx);
6305
+ return this.nIdx;
6152
6306
  }
6307
+ reset() {
6308
+ this.nVerts = 0;
6309
+ this.nIdx = 0;
6310
+ }
6311
+ dispose() {
6312
+ const gl = this.gl;
6313
+ gl.deleteBuffer(this.vbo);
6314
+ gl.deleteBuffer(this.ibo);
6315
+ gl.deleteVertexArray(this.vao);
6316
+ }
6317
+ /** Grow the CPU arrays so `vertices` / `indices` more fit. */
6318
+ reserve(vertices, indices) {
6319
+ const needVerts = (this.nVerts + vertices) * FLOATS_PER_VERTEX;
6320
+ if (needVerts > this.verts.length) {
6321
+ let len = this.verts.length;
6322
+ while (len < needVerts) len *= 2;
6323
+ const grown = new Float32Array(len);
6324
+ grown.set(this.verts);
6325
+ this.verts = grown;
6326
+ }
6327
+ const needIdx = this.nIdx + indices;
6328
+ if (needIdx > this.idx.length) {
6329
+ let len = this.idx.length;
6330
+ while (len < needIdx) len *= 2;
6331
+ const grown = new Uint32Array(len);
6332
+ grown.set(this.idx);
6333
+ this.idx = grown;
6334
+ }
6335
+ }
6336
+ /** Size both GPU buffers for at least what is staged, doubling to amortize. */
6337
+ growGpu(vertices, indices) {
6338
+ const gl = this.gl;
6339
+ if (vertices > this.vboVerts) {
6340
+ let capacity = Math.max(this.vboVerts, INITIAL_VERTICES);
6341
+ while (capacity < vertices) capacity *= 2;
6342
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
6343
+ gl.bufferData(gl.ARRAY_BUFFER, capacity * FLOATS_PER_VERTEX * 4, gl.DYNAMIC_DRAW);
6344
+ this.vboVerts = capacity;
6345
+ }
6346
+ if (indices > this.iboIdx) {
6347
+ let capacity = Math.max(this.iboIdx, INITIAL_INDICES);
6348
+ while (capacity < indices) capacity *= 2;
6349
+ gl.bindVertexArray(this.vao);
6350
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo);
6351
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, capacity * 4, gl.DYNAMIC_DRAW);
6352
+ gl.bindVertexArray(null);
6353
+ this.iboIdx = capacity;
6354
+ }
6355
+ }
6356
+ };
6357
+ var FRAME_UPLOADS = /* @__PURE__ */ new WeakMap();
6358
+ var FRAME_PROJ = /* @__PURE__ */ new WeakMap();
6359
+ function uploadedFor(ctx, prog) {
6360
+ let byProgram = FRAME_UPLOADS.get(ctx);
6361
+ if (!byProgram) {
6362
+ byProgram = /* @__PURE__ */ new WeakMap();
6363
+ FRAME_UPLOADS.set(ctx, byProgram);
6364
+ }
6365
+ let uploaded = byProgram.get(prog);
6366
+ if (!uploaded) {
6367
+ uploaded = {};
6368
+ byProgram.set(prog, uploaded);
6369
+ }
6370
+ return uploaded;
6371
+ }
6372
+ function sameValues(prev, next) {
6373
+ if (prev === void 0 || prev.length !== next.length) return false;
6374
+ for (let i = 0; i < prev.length; i++) if (prev[i] !== next[i]) return false;
6375
+ return true;
6376
+ }
6377
+ var BATCH_MODEL = mat3.identity();
6378
+ function projFor(ctx) {
6379
+ let proj = FRAME_PROJ.get(ctx);
6380
+ if (!proj) {
6381
+ proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
6382
+ FRAME_PROJ.set(ctx, proj);
6383
+ }
6384
+ return proj;
6385
+ }
6386
+ var COLOR_MATRIX_SCRATCH = new Float32Array(16);
6387
+ function setColorMatrixUniforms(ctx, prog, cm = ctx.state.colorMatrix) {
6388
+ const gl = ctx.gl;
6389
+ const uploaded = uploadedFor(ctx, prog);
6153
6390
  const mLoc = prog.uniform("u_colorMatrix");
6391
+ if (mLoc !== void 0) {
6392
+ for (let row = 0; row < 4; row++) {
6393
+ for (let col = 0; col < 4; col++) {
6394
+ COLOR_MATRIX_SCRATCH[col * 4 + row] = cm[row * 5 + col];
6395
+ }
6396
+ }
6397
+ if (!sameValues(uploaded.colorMatrix, COLOR_MATRIX_SCRATCH)) {
6398
+ gl.uniformMatrix4fv(mLoc, false, COLOR_MATRIX_SCRATCH);
6399
+ uploaded.colorMatrix = Float32Array.from(COLOR_MATRIX_SCRATCH);
6400
+ }
6401
+ }
6154
6402
  const bLoc = prog.uniform("u_colorBias");
6155
- if (mLoc !== void 0) gl.uniformMatrix4fv(mLoc, false, m4);
6156
- if (bLoc !== void 0) gl.uniform4f(bLoc, cm[4], cm[9], cm[14], cm[19]);
6403
+ if (bLoc !== void 0) {
6404
+ const bias = [cm[4], cm[9], cm[14], cm[19]];
6405
+ if (!sameValues(uploaded.colorBias, bias)) {
6406
+ gl.uniform4f(bLoc, bias[0], bias[1], bias[2], bias[3]);
6407
+ uploaded.colorBias = Float32Array.from(bias);
6408
+ }
6409
+ }
6157
6410
  }
6158
- function fillMeshHandle(ctx, path) {
6411
+ function fillMesh(ctx, path) {
6159
6412
  if (ctx.flattenTolerance !== void 0) {
6160
- return ctx.meshCache.uploadTransient(tessellate(path, { flattenTolerance: ctx.flattenTolerance }));
6413
+ return tessellate(path, { flattenTolerance: ctx.flattenTolerance });
6161
6414
  }
6162
- return ctx.meshCache.handleFor(getMesh(path));
6415
+ return getMesh(path);
6416
+ }
6417
+ function meshHandle(ctx, mesh) {
6418
+ if (ctx.flattenTolerance !== void 0) return ctx.meshCache.uploadTransient(mesh);
6419
+ return ctx.meshCache.handleFor(mesh);
6420
+ }
6421
+ function fillMeshHandle(ctx, path) {
6422
+ return meshHandle(ctx, fillMesh(ctx, path));
6163
6423
  }
6164
6424
  function dispatch(ctx, cmd) {
6165
6425
  switch (cmd.kind) {
@@ -6168,10 +6428,13 @@ function dispatch(ctx, cmd) {
6168
6428
  case "path":
6169
6429
  return drawPath(ctx, cmd);
6170
6430
  case "text":
6431
+ flushSolids(ctx);
6171
6432
  return drawText(ctx, cmd);
6172
6433
  case "image":
6434
+ flushSolids(ctx);
6173
6435
  return drawImage(ctx, cmd);
6174
6436
  case "shader":
6437
+ flushSolids(ctx);
6175
6438
  return drawShader(ctx, cmd);
6176
6439
  }
6177
6440
  }
@@ -6300,11 +6563,13 @@ function drawGroup(ctx, cmd) {
6300
6563
  "weasel: clip nesting depth exceeded (max 7). You can't nest more than 7 levels of clipped containers in a single draw tree. Flatten the hierarchy or compose poses outside the scene graph."
6301
6564
  );
6302
6565
  }
6566
+ flushSolids(ctx);
6303
6567
  pushClip(ctx, cmd.clip, newDepth);
6304
6568
  ctx.clipDepth = newDepth;
6305
6569
  }
6306
6570
  for (const child of cmd.children) dispatch(ctx, child);
6307
6571
  if (cmd.clip) {
6572
+ flushSolids(ctx);
6308
6573
  popClip(ctx, cmd.clip, ctx.clipDepth - 1);
6309
6574
  ctx.clipDepth -= 1;
6310
6575
  }
@@ -6313,38 +6578,98 @@ function drawGroup(ctx, cmd) {
6313
6578
  function drawPath(ctx, cmd) {
6314
6579
  if (!cmd.fill && !cmd.stroke) return;
6315
6580
  if (cmd.fill) {
6316
- const isSolidRectFast = cmd.path.kind === "rect" && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid") && (!cmd.vertexColors || cmd.vertexColors.length === 0) && ctx.rectVao !== null && ctx.rectVbo !== null;
6317
- if (isSolidRectFast && cmd.path.kind === "rect") {
6318
- drawRectFast(ctx, cmd.path, cmd.fill);
6581
+ const batchablePaint = (cmd.fill.fill === void 0 || cmd.fill.fill === "solid") && (!cmd.vertexColors || cmd.vertexColors.length === 0);
6582
+ const solid = cmd.fill;
6583
+ if (batchablePaint && cmd.path.kind === "rect") {
6584
+ pushRect(ctx, cmd.path, solid);
6319
6585
  } else {
6320
- const handle = fillMeshHandle(ctx, cmd.path);
6321
- if (cmd.vertexColors && cmd.vertexColors.length > 0 && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid")) {
6322
- drawPathFillVColor(ctx, cmd, cmd.fill, handle);
6323
- } else if (handle.requiresStencil) {
6324
- drawPathFillStencil(ctx, cmd.fill, handle);
6586
+ const mesh = fillMesh(ctx, cmd.path);
6587
+ if (batchablePaint && canBatchMesh(mesh)) {
6588
+ pushMesh(ctx, mesh, solid);
6325
6589
  } else {
6326
- drawPathFillByKind(ctx, cmd.fill, handle);
6590
+ flushSolids(ctx);
6591
+ const handle = meshHandle(ctx, mesh);
6592
+ if (cmd.vertexColors && cmd.vertexColors.length > 0 && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid")) {
6593
+ drawPathFillVColor(ctx, cmd, solid, handle);
6594
+ } else if (handle.requiresStencil) {
6595
+ drawPathFillStencil(ctx, cmd.fill, handle);
6596
+ } else {
6597
+ drawPathFillByKind(ctx, cmd.fill, handle);
6598
+ }
6327
6599
  }
6328
6600
  }
6329
6601
  }
6330
- if (cmd.stroke) {
6331
- drawPathStroke(ctx, cmd);
6602
+ if (cmd.stroke) drawPathStroke(ctx, cmd);
6603
+ }
6604
+ function isIdentityColorMatrix(cm) {
6605
+ return cm === IDENTITY_COLOR_MATRIX || sameValues(IDENTITY_COLOR_MATRIX, cm);
6606
+ }
6607
+ function stagedStateIsLive(ctx, staged) {
6608
+ if (staged.clipDepth !== ctx.clipDepth) return false;
6609
+ if (!staged.foldsAlpha && staged.alpha !== ctx.state.alpha) return false;
6610
+ const colorMatrix = ctx.state.colorMatrix;
6611
+ return staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix);
6612
+ }
6613
+ var MAX_BATCHED_MESH_VERTICES = 256;
6614
+ function canBatchMesh(mesh) {
6615
+ return !mesh.requiresStencil && mesh.vertices.length >> 1 <= MAX_BATCHED_MESH_VERTICES;
6616
+ }
6617
+ function stageSolid(ctx, vertices) {
6618
+ if (ctx.solidState !== void 0 && !stagedStateIsLive(ctx, ctx.solidState)) flushSolids(ctx);
6619
+ if (ctx.solidBatch.wouldOverflow(vertices)) flushSolids(ctx);
6620
+ if (ctx.solidState === void 0) {
6621
+ const colorMatrix = ctx.state.colorMatrix;
6622
+ const foldsAlpha = isIdentityColorMatrix(colorMatrix);
6623
+ ctx.solidState = {
6624
+ alpha: foldsAlpha ? 1 : ctx.state.alpha,
6625
+ colorMatrix,
6626
+ clipDepth: ctx.clipDepth,
6627
+ foldsAlpha
6628
+ };
6332
6629
  }
6630
+ return ctx.solidState;
6631
+ }
6632
+ function stagedColor(ctx, staged, paint) {
6633
+ const [r, g, b, a] = resolveColor(paint.color);
6634
+ return [r, g, b, a * (paint.opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1)];
6635
+ }
6636
+ function pushRect(ctx, rect, fill) {
6637
+ const staged = stageSolid(ctx, 4);
6638
+ const [r, g, b, a] = stagedColor(ctx, staged, fill);
6639
+ ctx.solidBatch.pushRect(
6640
+ rect.x,
6641
+ rect.y,
6642
+ rect.width,
6643
+ rect.height,
6644
+ ctx.state.transform,
6645
+ r,
6646
+ g,
6647
+ b,
6648
+ a
6649
+ );
6333
6650
  }
6334
- function drawRectFast(ctx, rect, fill) {
6651
+ function pushMesh(ctx, mesh, paint) {
6652
+ const staged = stageSolid(ctx, mesh.vertices.length >> 1);
6653
+ const [r, g, b, a] = stagedColor(ctx, staged, paint);
6654
+ ctx.solidBatch.pushMesh(mesh, ctx.state.transform, r, g, b, a);
6655
+ }
6656
+ function flushSolids(ctx) {
6657
+ const batch = ctx.solidBatch;
6658
+ const staged = ctx.solidState;
6659
+ if (batch.length === 0 || staged === void 0) return;
6335
6660
  const gl = ctx.gl;
6336
- const { x, y, width: w, height: h } = rect;
6337
- const corners = new Float32Array([x, y, x + w, y, x + w, y + h, x, y + h]);
6338
- gl.useProgram(ctx.pathFill.handle);
6339
- gl.bindVertexArray(ctx.rectVao);
6340
- gl.bindBuffer(gl.ARRAY_BUFFER, ctx.rectVbo);
6341
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, corners);
6342
- setProjAndModel(ctx, ctx.pathFill);
6343
- setSolidPaintUniforms(ctx, ctx.pathFill, fill.color, fill.opacity);
6344
- setColorMatrixUniforms(ctx, ctx.pathFill);
6345
- applyClipTest(ctx);
6346
- gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, 0);
6661
+ const prog = ctx.pathFillVColor;
6662
+ gl.useProgram(prog.handle);
6663
+ const indexCount = batch.uploadAndBind();
6664
+ setProjAndModel(ctx, prog, BATCH_MODEL);
6665
+ gl.uniform4f(prog.uniform("u_color"), 1, 1, 1, 1);
6666
+ gl.uniform1f(prog.uniform("u_alpha"), staged.alpha);
6667
+ setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
6668
+ applyClipTest(ctx, staged.clipDepth);
6669
+ gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
6347
6670
  gl.bindVertexArray(null);
6671
+ batch.reset();
6672
+ ctx.solidState = void 0;
6348
6673
  }
6349
6674
  function expandAnchorColors(perAnchor, handle) {
6350
6675
  const aA = handle.anchorA;
@@ -6389,11 +6714,18 @@ function drawPathFillVColor(ctx, cmd, fill, handle) {
6389
6714
  gl.bindVertexArray(null);
6390
6715
  gl.deleteBuffer(colorVbo);
6391
6716
  }
6392
- function setProjAndModel(ctx, prog) {
6717
+ function setProjAndModel(ctx, prog, model = ctx.state.transform) {
6393
6718
  const gl = ctx.gl;
6394
- const proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
6395
- gl.uniformMatrix3fv(prog.uniform("u_proj"), false, proj);
6396
- gl.uniformMatrix3fv(prog.uniform("u_model"), false, ctx.state.transform);
6719
+ const uploaded = uploadedFor(ctx, prog);
6720
+ const proj = projFor(ctx);
6721
+ if (!sameValues(uploaded.proj, proj)) {
6722
+ gl.uniformMatrix3fv(prog.uniform("u_proj"), false, proj);
6723
+ uploaded.proj = Float32Array.from(proj);
6724
+ }
6725
+ if (!sameValues(uploaded.model, model)) {
6726
+ gl.uniformMatrix3fv(prog.uniform("u_model"), false, model);
6727
+ uploaded.model = Float32Array.from(model);
6728
+ }
6397
6729
  }
6398
6730
  function setSolidPaintUniforms(ctx, prog, color, opacity) {
6399
6731
  const gl = ctx.gl;
@@ -6500,9 +6832,8 @@ function drawPathFillGradient(ctx, fill, handle) {
6500
6832
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
6501
6833
  gl.bindVertexArray(null);
6502
6834
  }
6503
- function applyClipTest(ctx) {
6835
+ function applyClipTest(ctx, depth = ctx.clipDepth) {
6504
6836
  const gl = ctx.gl;
6505
- const depth = ctx.clipDepth;
6506
6837
  if (depth === 0) {
6507
6838
  gl.disable(gl.STENCIL_TEST);
6508
6839
  return;
@@ -6586,6 +6917,7 @@ function drawPathStroke(ctx, cmd) {
6586
6917
  }
6587
6918
  const align = stroke.align ?? "center";
6588
6919
  if (cmd.path.kind === "polygon" && align !== "center") {
6920
+ flushSolids(ctx);
6589
6921
  drawPathStrokeStenciled(ctx, cmd, align);
6590
6922
  return;
6591
6923
  }
@@ -6596,6 +6928,12 @@ function drawPathStrokeUnclipped(ctx, cmd) {
6596
6928
  const solid = stroke.paint;
6597
6929
  const mesh = tessellateStroke(cmd.path, stroke, { flattenTolerance: ctx.flattenTolerance });
6598
6930
  if (mesh.indices.length === 0) return;
6931
+ const hasVColors = !!(stroke.vertexColors && stroke.vertexColors.length > 0);
6932
+ if (!hasVColors && canBatchMesh(mesh)) {
6933
+ pushMesh(ctx, mesh, solid);
6934
+ return;
6935
+ }
6936
+ flushSolids(ctx);
6599
6937
  const handle = ctx.meshCache.uploadTransient(mesh);
6600
6938
  const gl = ctx.gl;
6601
6939
  if (stroke.vertexColors && stroke.vertexColors.length > 0) {
@@ -6693,20 +7031,17 @@ function drawText(ctx, cmd) {
6693
7031
  const maxWidth = cmd.maxWidth ?? Infinity;
6694
7032
  const minScreen = ctx.textOutlineMinScreenSize ?? OUTLINE_MIN_SCREEN_PX;
6695
7033
  const outlineMinSize = Number.isFinite(minScreen) ? minScreen / modelScale(ctx.state.transform) : void 0;
6696
- const laid = cachedLayoutRuns(
6697
- cmd.runs,
6698
- { maxWidth, lineHeight, align, outlineMinSize },
6699
- { x: cmd.x, y: cmd.y }
6700
- );
7034
+ const laid = cachedLayoutRuns(cmd.runs, { maxWidth, lineHeight, align, outlineMinSize });
6701
7035
  if (laid.groups.length === 0 && laid.decorations.length === 0) return;
6702
- const dy = verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
7036
+ const dx = cmd.x;
7037
+ const dy = cmd.y + verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
6703
7038
  const gl = ctx.gl;
6704
7039
  applyClipTest(ctx);
6705
7040
  const preparedPrograms = /* @__PURE__ */ new Set();
6706
7041
  let currentProg = null;
6707
7042
  for (const group of laid.groups) {
6708
7043
  if (group.source === "outline") {
6709
- drawTextOutlineGroup(ctx, group, dy);
7044
+ drawTextOutlineGroup(ctx, group, dx, dy);
6710
7045
  currentProg = null;
6711
7046
  continue;
6712
7047
  }
@@ -6721,31 +7056,31 @@ function drawText(ctx, cmd) {
6721
7056
  setColorMatrixUniforms(ctx, prog);
6722
7057
  gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
6723
7058
  }
6724
- drawTextGroup(ctx, group, prog, dy);
7059
+ drawTextGroup(ctx, group, prog, dx, dy);
6725
7060
  }
6726
- drawTextDecorations(ctx, laid.decorations, dy);
7061
+ drawTextDecorations(ctx, laid.decorations, dx, dy);
6727
7062
  }
6728
7063
  var SYNTHETIC_ITALIC_RADIANS = 0.2094;
6729
- function drawTextOutlineGroup(ctx, group, dy) {
6730
- const mesh = outlineGroupMesh(group, dy);
7064
+ function drawTextOutlineGroup(ctx, group, dx, dy) {
7065
+ const mesh = outlineGroupMesh(group, dx, dy);
6731
7066
  if (mesh) drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
6732
7067
  if (!group.stroke) return;
6733
- const strokeMesh = outlineGroupStrokeMesh(group, dy);
7068
+ const strokeMesh = outlineGroupStrokeMesh(group, dx, dy);
6734
7069
  if (strokeMesh) {
6735
7070
  drawPathFillByKind(ctx, group.stroke.paint, ctx.meshCache.uploadTransient(strokeMesh));
6736
7071
  }
6737
7072
  }
6738
- function outlineGroupMesh(group, dy) {
6739
- return mergeGlyphMeshes(group, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
7073
+ function outlineGroupMesh(group, dx, dy) {
7074
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
6740
7075
  }
6741
- function outlineGroupStrokeMesh(group, dy) {
7076
+ function outlineGroupStrokeMesh(group, dx, dy) {
6742
7077
  const stroke = group.stroke;
6743
7078
  if (!stroke) return null;
6744
7079
  const width = stroke.width ?? 1;
6745
7080
  if (!(width > 0)) return null;
6746
- return mergeGlyphMeshes(group, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
7081
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
6747
7082
  }
6748
- function mergeGlyphMeshes(group, dy, meshFor) {
7083
+ function mergeGlyphMeshes(group, dx, dy, meshFor) {
6749
7084
  const parts = [];
6750
7085
  let vertexFloats = 0;
6751
7086
  let indexCount = 0;
@@ -6768,7 +7103,7 @@ function mergeGlyphMeshes(group, dy, meshFor) {
6768
7103
  for (let k = 0; k < mesh.vertices.length; k += 2) {
6769
7104
  const ex = mesh.vertices[k];
6770
7105
  const ey = mesh.vertices[k + 1];
6771
- vertices[vi++] = x + (ex - ey * shear) * scale2;
7106
+ vertices[vi++] = x + dx + (ex - ey * shear) * scale2;
6772
7107
  vertices[vi++] = baselineY + dy + ey * scale2;
6773
7108
  }
6774
7109
  for (let k = 0; k < mesh.indices.length; k++) indices[ii++] = base + mesh.indices[k];
@@ -6776,7 +7111,7 @@ function mergeGlyphMeshes(group, dy, meshFor) {
6776
7111
  }
6777
7112
  return { vertices, indices };
6778
7113
  }
6779
- function drawTextDecorations(ctx, decorations, dy) {
7114
+ function drawTextDecorations(ctx, decorations, dx, dy) {
6780
7115
  if (decorations.length === 0) return;
6781
7116
  const batches = /* @__PURE__ */ new Map();
6782
7117
  for (const d of decorations) {
@@ -6799,14 +7134,15 @@ function drawTextDecorations(ctx, decorations, dy) {
6799
7134
  const vertices = new Float32Array(rects.length * 4 * 2);
6800
7135
  let vi = 0;
6801
7136
  for (const d of rects) {
7137
+ const dx0 = d.x0 + dx, dx1 = d.x1 + dx;
6802
7138
  const dy0 = d.y0 + dy, dy1 = d.y1 + dy;
6803
- vertices[vi++] = d.x0;
7139
+ vertices[vi++] = dx0;
6804
7140
  vertices[vi++] = dy0;
6805
- vertices[vi++] = d.x1;
7141
+ vertices[vi++] = dx1;
6806
7142
  vertices[vi++] = dy0;
6807
- vertices[vi++] = d.x0;
7143
+ vertices[vi++] = dx0;
6808
7144
  vertices[vi++] = dy1;
6809
- vertices[vi++] = d.x1;
7145
+ vertices[vi++] = dx1;
6810
7146
  vertices[vi++] = dy1;
6811
7147
  }
6812
7148
  const indices = new Uint32Array(rects.length * 6);
@@ -6845,7 +7181,7 @@ function drawTextDecorations(ctx, decorations, dy) {
6845
7181
  gl.deleteBuffer(ibo);
6846
7182
  }
6847
7183
  }
6848
- function drawTextGroup(ctx, group, prog, dy) {
7184
+ function drawTextGroup(ctx, group, prog, dx, dy) {
6849
7185
  if (group.source === "canvas") {
6850
7186
  if (!syncDynamicPageTexture(ctx.textureCache, group.page)) return;
6851
7187
  } else {
@@ -6856,23 +7192,24 @@ function drawTextGroup(ctx, group, prog, dy) {
6856
7192
  const vertices = new Float32Array(group.quads.length * 4 * 5);
6857
7193
  let vi = 0;
6858
7194
  for (const q of group.quads) {
7195
+ const x0 = q.x0 + dx, x1 = q.x1 + dx;
6859
7196
  const y0 = q.y0 + dy, y1 = q.y1 + dy, by = q.baselineY + dy;
6860
- vertices[vi++] = q.x0;
7197
+ vertices[vi++] = x0;
6861
7198
  vertices[vi++] = y0;
6862
7199
  vertices[vi++] = q.u0;
6863
7200
  vertices[vi++] = q.v0;
6864
7201
  vertices[vi++] = by;
6865
- vertices[vi++] = q.x1;
7202
+ vertices[vi++] = x1;
6866
7203
  vertices[vi++] = y0;
6867
7204
  vertices[vi++] = q.u1;
6868
7205
  vertices[vi++] = q.v0;
6869
7206
  vertices[vi++] = by;
6870
- vertices[vi++] = q.x0;
7207
+ vertices[vi++] = x0;
6871
7208
  vertices[vi++] = y1;
6872
7209
  vertices[vi++] = q.u0;
6873
7210
  vertices[vi++] = q.v1;
6874
7211
  vertices[vi++] = by;
6875
- vertices[vi++] = q.x1;
7212
+ vertices[vi++] = x1;
6876
7213
  vertices[vi++] = y1;
6877
7214
  vertices[vi++] = q.u1;
6878
7215
  vertices[vi++] = q.v1;
@@ -7085,10 +7422,7 @@ var WeaselRenderer = class {
7085
7422
  programRegistry = /* @__PURE__ */ new Map();
7086
7423
  quadVbo = null;
7087
7424
  quadIbo = null;
7088
- /** Shared rect-fill geometry: 4 verts × 2 floats (dynamic), 6-index static IBO. */
7089
- rectVao = null;
7090
- rectVbo = null;
7091
- rectIbo = null;
7425
+ solidBatch;
7092
7426
  groupState = new GroupState();
7093
7427
  widthCss;
7094
7428
  heightCss;
@@ -7163,31 +7497,7 @@ var WeaselRenderer = class {
7163
7497
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
7164
7498
  this.gradRampCache = new GradientRampCache(this.gl);
7165
7499
  this.uploadQuadGeometry();
7166
- this.uploadRectGeometry(aPos);
7167
- }
7168
- /** Allocate the shared rect VAO + dynamic VBO (4 verts × 2 floats) + static
7169
- * IBO (6 indices). drawRectFast bufferSubData's the 4 corner coords each
7170
- * draw, avoiding per-rect buffer allocation in animated demos. */
7171
- uploadRectGeometry(aPositionLoc) {
7172
- const gl = this.gl;
7173
- this.rectVao = gl.createVertexArray();
7174
- this.rectVbo = gl.createBuffer();
7175
- this.rectIbo = gl.createBuffer();
7176
- if (!this.rectVao || !this.rectVbo || !this.rectIbo) {
7177
- throw new Error("WeaselRenderer: failed to create rect-fast geometry");
7178
- }
7179
- gl.bindVertexArray(this.rectVao);
7180
- gl.bindBuffer(gl.ARRAY_BUFFER, this.rectVbo);
7181
- gl.bufferData(gl.ARRAY_BUFFER, 8 * 4, gl.DYNAMIC_DRAW);
7182
- gl.enableVertexAttribArray(aPositionLoc);
7183
- gl.vertexAttribPointer(aPositionLoc, 2, gl.FLOAT, false, 0, 0);
7184
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.rectIbo);
7185
- gl.bufferData(
7186
- gl.ELEMENT_ARRAY_BUFFER,
7187
- new Uint32Array([0, 1, 2, 0, 2, 3]),
7188
- gl.STATIC_DRAW
7189
- );
7190
- gl.bindVertexArray(null);
7500
+ this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
7191
7501
  }
7192
7502
  uploadQuadGeometry() {
7193
7503
  const gl = this.gl;
@@ -7274,7 +7584,7 @@ var WeaselRenderer = class {
7274
7584
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
7275
7585
  this.gradRampCache = new GradientRampCache(this.gl);
7276
7586
  this.uploadQuadGeometry();
7277
- this.uploadRectGeometry(aPos);
7587
+ this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
7278
7588
  for (const id of this.programRegistry.keys()) {
7279
7589
  const src = getProgramSource(id);
7280
7590
  if (!src) continue;
@@ -7334,9 +7644,7 @@ var WeaselRenderer = class {
7334
7644
  this.gradRampCache.free();
7335
7645
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
7336
7646
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
7337
- if (this.rectVbo) gl.deleteBuffer(this.rectVbo);
7338
- if (this.rectIbo) gl.deleteBuffer(this.rectIbo);
7339
- if (this.rectVao) gl.deleteVertexArray(this.rectVao);
7647
+ this.solidBatch.dispose();
7340
7648
  }
7341
7649
  /**
7342
7650
  * Draw one frame.
@@ -7369,8 +7677,7 @@ var WeaselRenderer = class {
7369
7677
  programRegistry: this.programRegistry,
7370
7678
  quadVbo: this.quadVbo,
7371
7679
  quadIbo: this.quadIbo,
7372
- rectVao: this.rectVao,
7373
- rectVbo: this.rectVbo,
7680
+ solidBatch: this.solidBatch,
7374
7681
  state: this.groupState,
7375
7682
  widthCss: this.widthCss,
7376
7683
  heightCss: this.heightCss,
@@ -7380,6 +7687,7 @@ var WeaselRenderer = class {
7380
7687
  viewMatrix
7381
7688
  };
7382
7689
  for (const cmd of commands) dispatch(ctx, cmd);
7690
+ flushSolids(ctx);
7383
7691
  this.meshCache.freeTransient();
7384
7692
  }
7385
7693
  resize(dims) {
@@ -7409,6 +7717,10 @@ var WeaselRenderer = class {
7409
7717
  return this.pathFillVColor;
7410
7718
  }
7411
7719
  /** @internal */
7720
+ _solidBatch() {
7721
+ return this.solidBatch;
7722
+ }
7723
+ /** @internal */
7412
7724
  _textSdf() {
7413
7725
  return this.textSdf;
7414
7726
  }
@@ -7794,12 +8106,7 @@ function moveGestureAdapter(scene) {
7794
8106
  return {
7795
8107
  getNode: (id) => scene.get(asNodeId(id)),
7796
8108
  getNodes: () => {
7797
- const out = [];
7798
- for (const id of scene.renderOrder()) {
7799
- const n = scene.get(id);
7800
- if (n) out.push(n);
7801
- }
7802
- return out;
8109
+ return [...scene.renderOrderNodes()];
7803
8110
  },
7804
8111
  getPose: (id) => scene.get(asNodeId(id)).pose,
7805
8112
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
@@ -8365,12 +8672,7 @@ function defaultCommitAdapter(scene) {
8365
8672
  return {
8366
8673
  getNode: (id) => scene.get(asNodeId(id)),
8367
8674
  getNodes: () => {
8368
- const out = [];
8369
- for (const id of scene.renderOrder()) {
8370
- const n = scene.get(id);
8371
- if (n) out.push(n);
8372
- }
8373
- return out;
8675
+ return [...scene.renderOrderNodes()];
8374
8676
  },
8375
8677
  getPose: (id) => scene.get(asNodeId(id)).pose,
8376
8678
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
@@ -12851,18 +13153,39 @@ function modsCount(mods) {
12851
13153
  }
12852
13154
  function targetRank(target) {
12853
13155
  if (target === void 0) return 0;
12854
- if (typeof target !== "string") return 1;
12855
- if (target.startsWith("kind:")) return target.endsWith(":selected") ? 3 : 2;
12856
- if (target.startsWith("affordance:")) return 2;
12857
- return 1;
13156
+ const form = parseTargetSpec(target);
13157
+ if (form === null) return 1;
13158
+ switch (form.form) {
13159
+ case "kind":
13160
+ return form.requireSelected ? 3 : 2;
13161
+ case "affordance":
13162
+ return 2;
13163
+ case "body":
13164
+ return 1;
13165
+ case "predicate":
13166
+ return 1;
13167
+ default: {
13168
+ return 1;
13169
+ }
13170
+ }
12858
13171
  }
12859
13172
  function targetConsultsAffordance(specTarget) {
12860
13173
  if (specTarget === void 0) return false;
12861
- if (typeof specTarget === "object" && specTarget !== null && "kindOf" in specTarget) {
12862
- const kindOf2 = specTarget.kindOf;
12863
- return kindOf2?.readsAffordance !== false;
13174
+ const form = parseTargetSpec(specTarget);
13175
+ if (form === null) return false;
13176
+ switch (form.form) {
13177
+ case "predicate":
13178
+ return form.kindOf.readsAffordance !== false;
13179
+ case "affordance":
13180
+ return true;
13181
+ case "body":
13182
+ return false;
13183
+ case "kind":
13184
+ return false;
13185
+ default: {
13186
+ return false;
13187
+ }
12864
13188
  }
12865
- return typeof specTarget === "string" && specTarget.startsWith("affordance:");
12866
13189
  }
12867
13190
  function specTargetOf(spec) {
12868
13191
  return "target" in spec ? spec.target : void 0;
@@ -12905,7 +13228,7 @@ function reportDeadClaim(owner, warn) {
12905
13228
  );
12906
13229
  }
12907
13230
  function specificity(spec) {
12908
- const t = targetRank("target" in spec ? spec.target : void 0);
13231
+ const t = targetRank(specTargetOf(spec));
12909
13232
  const mods = "mods" in spec ? spec.mods : void 0;
12910
13233
  const m = modsCount(mods);
12911
13234
  const p = "phase" in spec && spec.phase !== void 0 ? 1 : 0;
@@ -15439,21 +15762,18 @@ function textLineBoxes(pose, opts = {}) {
15439
15762
  const style = resolveTextStyle(pose.style);
15440
15763
  const source = pose.runs && pose.runs.length > 0 ? pose.runs : pose.text;
15441
15764
  const runs = resolveRuns(toRuns(source), style);
15442
- const laid = layoutRuns(
15443
- runs,
15444
- {
15445
- maxWidth: opts.maxWidth ?? pose.width,
15446
- lineHeight: style.lineHeight,
15447
- align: style.align
15448
- },
15449
- { x: pose.x, y: pose.y }
15450
- );
15451
- const dy = verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height);
15765
+ const laid = layoutRuns(runs, {
15766
+ maxWidth: opts.maxWidth ?? pose.width,
15767
+ lineHeight: style.lineHeight,
15768
+ align: style.align
15769
+ });
15770
+ const dx = pose.x;
15771
+ const dy = pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height);
15452
15772
  const out = [];
15453
15773
  for (const line of laid.lines) {
15454
15774
  if (!opts.includeEmpty && line.x1 <= line.x0) continue;
15455
15775
  out.push({
15456
- x: line.x0 - padding,
15776
+ x: line.x0 + dx - padding,
15457
15777
  y: line.y0 + dy - padding,
15458
15778
  width: line.x1 - line.x0 + padding * 2,
15459
15779
  height: line.y1 - line.y0 + padding * 2
@@ -18168,6 +18488,13 @@ function wrapWithPoseRotation(cmds, pose) {
18168
18488
  children: cmds
18169
18489
  }];
18170
18490
  }
18491
+ function wrapNodeOutput(cmds, pose, alpha) {
18492
+ const rotated = wrapWithPoseRotation(cmds, pose);
18493
+ if (alpha !== 1 && rotated.length > 0) {
18494
+ return [{ kind: "group", alpha, children: rotated }];
18495
+ }
18496
+ return rotated;
18497
+ }
18171
18498
  function CursorCoordsHud({ canvasRef, viewRef, offset }) {
18172
18499
  const [state, setState] = useState({
18173
18500
  client: { x: 0, y: 0 },
@@ -18415,12 +18742,7 @@ function buildSceneLayer(cfg, adapter, debugSink, boundsOfFn, hideIds, slot) {
18415
18742
  const oy = pose.y ?? (b ? b.y : 0);
18416
18743
  debugSink.recordOrigin(obj.id, { x: ox, y: oy });
18417
18744
  }
18418
- const rotated = wrapWithPoseRotation(cmds, pose);
18419
- const alpha = cfg.alphaFor ? cfg.alphaFor(obj.id) : 1;
18420
- if (alpha !== 1 && rotated.length > 0) {
18421
- return [{ kind: "group", alpha, children: rotated }];
18422
- }
18423
- return rotated;
18745
+ return wrapNodeOutput(cmds, pose, cfg.alphaFor ? cfg.alphaFor(obj.id) : 1);
18424
18746
  };
18425
18747
  const hierarchicalAdapter = cfg.toPose ? {
18426
18748
  ...a,
@@ -18444,13 +18766,8 @@ function buildSceneLayer(cfg, adapter, debugSink, boundsOfFn, hideIds, slot) {
18444
18766
  const pose = toPose(obj);
18445
18767
  if (drawOne) {
18446
18768
  const cmds = drawOne(obj, pose, view);
18447
- const rotated = wrapWithPoseRotation(cmds, pose);
18448
- const alpha = cfg.alphaFor ? cfg.alphaFor(obj.id) : 1;
18449
- if (alpha !== 1 && rotated.length > 0) {
18450
- children.push({ kind: "group", alpha, children: rotated });
18451
- } else {
18452
- for (const cmd of rotated) children.push(cmd);
18453
- }
18769
+ const wrapped = wrapNodeOutput(cmds, pose, cfg.alphaFor ? cfg.alphaFor(obj.id) : 1);
18770
+ for (const cmd of wrapped) children.push(cmd);
18454
18771
  }
18455
18772
  if (debugSink) {
18456
18773
  const b = boundsOfFn ? boundsOfFn(obj.id) : null;
@@ -19164,6 +19481,7 @@ function createScene(options) {
19164
19481
  return i;
19165
19482
  }
19166
19483
  function rebuildLayerIndex() {
19484
+ invalidateOrder();
19167
19485
  state.layerIndex.clear();
19168
19486
  for (let i = 0; i < state.layers.length; i++) {
19169
19487
  state.layerIndex.set(state.layers[i].id, i);
@@ -19189,6 +19507,7 @@ function createScene(options) {
19189
19507
  const idx = sibs.indexOf(id);
19190
19508
  if (idx < 0) throw new Error(`Scene: node "${id}" not found in its parent's children`);
19191
19509
  sibs.splice(idx, 1);
19510
+ invalidateOrder();
19192
19511
  return { parent: node.parent, index: idx };
19193
19512
  }
19194
19513
  function attach(id, parent, index) {
@@ -19197,6 +19516,7 @@ function createScene(options) {
19197
19516
  sibs.splice(i, 0, id);
19198
19517
  const node = requireNode(id);
19199
19518
  node.parent = parent;
19519
+ invalidateOrder();
19200
19520
  }
19201
19521
  function descendants(id, out) {
19202
19522
  const n = state.nodes.get(id);
@@ -19276,9 +19596,11 @@ function createScene(options) {
19276
19596
  apply: (p) => {
19277
19597
  requireLayerIndex(p.to);
19278
19598
  requireNode(p.id).layer = p.to;
19599
+ invalidateOrder();
19279
19600
  },
19280
19601
  revert: (p) => {
19281
19602
  requireNode(p.id).layer = p.from;
19603
+ invalidateOrder();
19282
19604
  }
19283
19605
  });
19284
19606
  registerKitOp("kit:move", {
@@ -19417,21 +19739,98 @@ function createScene(options) {
19417
19739
  registered.set(k, h);
19418
19740
  }
19419
19741
  }
19420
- function* renderOrderInternal() {
19421
- for (const layer of state.layers) {
19422
- const stack = [...state.roots].reverse();
19423
- while (stack.length > 0) {
19424
- const id = stack.pop();
19425
- const node = state.nodes.get(id);
19426
- if (!node) continue;
19427
- if (node.layer === layer.id) yield id;
19428
- if (node.kind === "container") {
19429
- for (let i = node.children.length - 1; i >= 0; i--) {
19430
- stack.push(node.children[i]);
19431
- }
19742
+ function renderOrderInternal() {
19743
+ return state.layers.length === 1 ? renderOrderFlat() : renderOrderBucketed();
19744
+ }
19745
+ function renderOrderNodesInternal() {
19746
+ return state.layers.length === 1 ? renderOrderNodesFlat() : renderOrderNodesBucketed();
19747
+ }
19748
+ let structureGeneration = 0;
19749
+ let orderCache = { gen: 0 };
19750
+ function invalidateOrder() {
19751
+ structureGeneration++;
19752
+ }
19753
+ function currentOrderCache() {
19754
+ if (orderCache.gen !== structureGeneration) {
19755
+ orderCache = { gen: structureGeneration };
19756
+ }
19757
+ return orderCache;
19758
+ }
19759
+ function renderOrderFlat() {
19760
+ const only = state.layers[0].id;
19761
+ const out = [];
19762
+ const stack = [...state.roots].reverse();
19763
+ while (stack.length > 0) {
19764
+ const id = stack.pop();
19765
+ const node = state.nodes.get(id);
19766
+ if (!node) continue;
19767
+ if (node.layer === only) out.push(id);
19768
+ if (node.kind === "container") {
19769
+ for (let i = node.children.length - 1; i >= 0; i--) {
19770
+ stack.push(node.children[i]);
19771
+ }
19772
+ }
19773
+ }
19774
+ return out;
19775
+ }
19776
+ function renderOrderNodesFlat() {
19777
+ const only = state.layers[0].id;
19778
+ const out = [];
19779
+ const stack = [...state.roots].reverse();
19780
+ while (stack.length > 0) {
19781
+ const id = stack.pop();
19782
+ const node = state.nodes.get(id);
19783
+ if (!node) continue;
19784
+ if (node.layer === only) out.push(node);
19785
+ if (node.kind === "container") {
19786
+ for (let i = node.children.length - 1; i >= 0; i--) {
19787
+ stack.push(node.children[i]);
19432
19788
  }
19433
19789
  }
19434
19790
  }
19791
+ return out;
19792
+ }
19793
+ function renderOrderBucketed() {
19794
+ const buckets = state.layers.map(() => []);
19795
+ const stack = [...state.roots].reverse();
19796
+ while (stack.length > 0) {
19797
+ const id = stack.pop();
19798
+ const node = state.nodes.get(id);
19799
+ if (!node) continue;
19800
+ const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
19801
+ if (bucket) bucket.push(id);
19802
+ if (node.kind === "container") {
19803
+ for (let i = node.children.length - 1; i >= 0; i--) {
19804
+ stack.push(node.children[i]);
19805
+ }
19806
+ }
19807
+ }
19808
+ const out = [];
19809
+ for (const bucket of buckets) {
19810
+ for (const id of bucket) out.push(id);
19811
+ }
19812
+ return out;
19813
+ }
19814
+ function renderOrderNodesBucketed() {
19815
+ const buckets = state.layers.map(() => []);
19816
+ const stack = [...state.roots].reverse();
19817
+ while (stack.length > 0) {
19818
+ const id = stack.pop();
19819
+ const node = state.nodes.get(id);
19820
+ if (!node) continue;
19821
+ const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
19822
+ if (bucket) bucket.push(node);
19823
+ if (node.kind === "container") {
19824
+ for (let i = node.children.length - 1; i >= 0; i--) {
19825
+ stack.push(node.children[i]);
19826
+ }
19827
+ }
19828
+ }
19829
+ const out = [];
19830
+ for (const bucket of buckets) {
19831
+ for (const node of bucket) out.push(node);
19832
+ }
19833
+ return out;
19435
19834
  }
19436
19835
  const scene = {
19437
19836
  get nodes() {
@@ -19460,7 +19859,12 @@ function createScene(options) {
19460
19859
  return out;
19461
19860
  },
19462
19861
  renderOrder() {
19463
- return renderOrderInternal();
19862
+ const c = currentOrderCache();
19863
+ return c.ids ??= renderOrderInternal();
19864
+ },
19865
+ renderOrderNodes() {
19866
+ const c = currentOrderCache();
19867
+ return c.nodes ??= renderOrderNodesInternal();
19464
19868
  },
19465
19869
  add(spec) {
19466
19870
  const id = spec.id ?? generateId();
@@ -19768,9 +20172,8 @@ function createScene(options) {
19768
20172
  },
19769
20173
  toJSON() {
19770
20174
  const nodes = [];
19771
- for (const id of renderOrderInternal()) {
19772
- const n = state.nodes.get(id);
19773
- if (!n) continue;
20175
+ for (const n of renderOrderNodesInternal()) {
20176
+ const id = n.id;
19774
20177
  const out = {
19775
20178
  id,
19776
20179
  kind: n.kind,
@@ -19804,6 +20207,7 @@ function createScene(options) {
19804
20207
  state.roots.length = 0;
19805
20208
  state.layers.length = 0;
19806
20209
  state.layerIndex.clear();
20210
+ invalidateOrder();
19807
20211
  for (let i = 0; i < json.systemLayers.length; i++) {
19808
20212
  const spec = json.systemLayers[i];
19809
20213
  if (state.layerIndex.has(spec.id)) {
@@ -20072,9 +20476,8 @@ function sceneToAdapter(scene, options = {}) {
20072
20476
  getNodes() {
20073
20477
  const visible = visibleLayers();
20074
20478
  const out = [];
20075
- for (const id of scene.renderOrder()) {
20076
- const n = scene.get(id);
20077
- if (n && visible.has(n.layer)) out.push(n);
20479
+ for (const n of scene.renderOrderNodes()) {
20480
+ if (visible.has(n.layer)) out.push(n);
20078
20481
  }
20079
20482
  return out;
20080
20483
  },
@@ -20405,9 +20808,7 @@ function useSceneSelectTool(args) {
20405
20808
  // path descriptor for a tighter test.
20406
20809
  hitTestArea: (rect) => {
20407
20810
  const hits = [];
20408
- for (const nid of scene.renderOrder()) {
20409
- const n = scene.get(nid);
20410
- if (!n) continue;
20811
+ for (const n of scene.renderOrderNodes()) {
20411
20812
  if (isPathLike2(n.pose) && pathPoseDescriptor.intersectsRect) {
20412
20813
  if (pathPoseDescriptor.intersectsRect(n.pose, rect)) hits.push(n.id);
20413
20814
  continue;
@@ -20445,9 +20846,8 @@ function useSceneSelectTool(args) {
20445
20846
  }
20446
20847
  const tolerance = pickTolerancePx / meanScale(getView?.()?.scale ?? { x: 1, y: 1 });
20447
20848
  const out = [];
20448
- for (const id of scene.renderOrder()) {
20449
- const n = scene.get(id);
20450
- if (!n || !poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
20849
+ for (const n of scene.renderOrderNodes()) {
20850
+ if (!poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
20451
20851
  if (shapePicking && !shapeCoversPoint(n, n.pose, wx, wy, { tolerance })) continue;
20452
20852
  out.push(n.id);
20453
20853
  }
@@ -21564,23 +21964,25 @@ function hitTestAreaPolygon(scene, area, areaBounds) {
21564
21964
  const ab = areaBounds ?? boundsOf(area);
21565
21965
  if (!ab) return [];
21566
21966
  const hits = [];
21567
- for (const id of scene.renderOrder()) {
21568
- const node = scene.get(id);
21569
- if (!node || node.kind === "container") continue;
21967
+ const order = scene.renderOrderNodes();
21968
+ for (let i = 0; i < order.length; i++) {
21969
+ const node = order[i];
21970
+ if (node.kind === "container") continue;
21570
21971
  const pose = node.pose;
21571
- const b = aabbOfPose(pose);
21972
+ const silhouette = pose !== null && typeof pose === "object" && pose.kind === "polygon";
21973
+ const b = silhouette ? nodeMemo(node, "aabb", pose, () => aabbOfPose(pose)) : aabbOfPose(pose);
21572
21974
  if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) {
21573
21975
  continue;
21574
21976
  }
21575
21977
  if (b.x >= ab.x + ab.width || b.x + b.width <= ab.x || b.y >= ab.y + ab.height || b.y + b.height <= ab.y) {
21576
21978
  continue;
21577
21979
  }
21578
- if (!isPathLike2(pose) || pose.kind === "rect") {
21579
- hits.push(id);
21980
+ if (!silhouette) {
21981
+ hits.push(node.id);
21580
21982
  continue;
21581
21983
  }
21582
21984
  if (silhouetteOverlapsArea(pose.coords, area)) {
21583
- hits.push(id);
21985
+ hits.push(node.id);
21584
21986
  }
21585
21987
  }
21586
21988
  return hits;
@@ -23735,14 +24137,25 @@ inferredNodeRouting.map((e) => ({
23735
24137
  schema: shapeSchema({ text: e.name === "text" })
23736
24138
  }));
23737
24139
  var RENDERER_CACHE = /* @__PURE__ */ new WeakMap();
23738
- function buildSceneViewCommands(scene, view, drawOne, extraCommands) {
23739
- const children = [];
23740
- for (const id of scene.renderOrder()) {
23741
- const node = scene.get(id);
23742
- if (!node) continue;
23743
- const cmds = drawOne(node, node.pose, view);
23744
- for (const cmd of cmds) children.push(cmd);
23745
- }
24140
+ function sceneAsHierarchy(scene) {
24141
+ return {
24142
+ getLayers: () => scene.layers.map((l) => ({ id: l.id, visible: l.visible })),
24143
+ getNode: (id) => scene.get(id),
24144
+ getChildren: (parentId) => {
24145
+ if (parentId === null) return scene.roots;
24146
+ const node = scene.get(parentId);
24147
+ return node && node.kind === "container" ? node.children : [];
24148
+ },
24149
+ getPose: (id) => scene.get(id).pose
24150
+ };
24151
+ }
24152
+ function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor) {
24153
+ const wrappedDrawOne = (node, pose, v) => wrapNodeOutput(drawOne(node, pose, v), pose, alphaFor ? alphaFor(node.id) : 1);
24154
+ const children = buildSceneTree(
24155
+ sceneAsHierarchy(scene),
24156
+ wrappedDrawOne,
24157
+ view
24158
+ );
23746
24159
  if (extraCommands && extraCommands.length > 0) {
23747
24160
  for (const cmd of extraCommands) children.push(cmd);
23748
24161
  }
@@ -23753,7 +24166,7 @@ function buildSceneViewCommands(scene, view, drawOne, extraCommands) {
23753
24166
  }];
23754
24167
  }
23755
24168
  function renderSceneToCanvas(args) {
23756
- const { canvas, scene, view, width, height, drawOne, extraCommands } = args;
24169
+ const { canvas, scene, view, width, height, drawOne, extraCommands, alphaFor } = args;
23757
24170
  const dpr = args.dpr ?? (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
23758
24171
  let entry = RENDERER_CACHE.get(canvas);
23759
24172
  if (!entry) {
@@ -23781,11 +24194,11 @@ function renderSceneToCanvas(args) {
23781
24194
  entry.height = height;
23782
24195
  entry.dpr = dpr;
23783
24196
  }
23784
- const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands);
24197
+ const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor);
23785
24198
  entry.renderer.render(commands, viewToMat3(view));
23786
24199
  }
23787
24200
  function SceneViewCanvasInner(props) {
23788
- const { scene, view, width, height, drawOne, extraCommands, className, canvasRef } = props;
24201
+ const { scene, view, width, height, drawOne, extraCommands, alphaFor, className, canvasRef } = props;
23789
24202
  useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);
23790
24203
  const localRef = useRef(null);
23791
24204
  const setCanvasRef = (el) => {
@@ -23806,7 +24219,8 @@ function SceneViewCanvasInner(props) {
23806
24219
  width,
23807
24220
  height,
23808
24221
  drawOne,
23809
- extraCommands
24222
+ extraCommands,
24223
+ alphaFor
23810
24224
  });
23811
24225
  });
23812
24226
  return /* @__PURE__ */ jsx(
@@ -23903,6 +24317,7 @@ function MinimapCanvasInner(props) {
23903
24317
  width,
23904
24318
  height,
23905
24319
  drawOne,
24320
+ alphaFor,
23906
24321
  fit = "scene",
23907
24322
  poseBounds,
23908
24323
  indicatorStyle,
@@ -24014,6 +24429,7 @@ function MinimapCanvasInner(props) {
24014
24429
  height,
24015
24430
  drawOne,
24016
24431
  extraCommands,
24432
+ alphaFor,
24017
24433
  className,
24018
24434
  canvasRef: setCanvasRef
24019
24435
  }
@@ -24157,5 +24573,5 @@ function oklchToOklab(L, C, h) {
24157
24573
  }
24158
24574
 
24159
24575
  export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, UnknownIcon, buildSceneViewCommands, computeFitView2, computeIndicatorCommand, eligibleContribution, evaluateEnabled, oklabToSrgbU8, oklchToOklab, renderSceneToCanvas, rgbaToHex, useActionsRegistry, useOptionalDepRegistry };
24160
- //# sourceMappingURL=chunk-XVBXYKFJ.js.map
24161
- //# sourceMappingURL=chunk-XVBXYKFJ.js.map
24576
+ //# sourceMappingURL=chunk-ISEK5LXT.js.map
24577
+ //# sourceMappingURL=chunk-ISEK5LXT.js.map