gitframes 1.3.0 → 1.3.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.
Files changed (35) hide show
  1. package/dist/audio/index.mjs +4 -4
  2. package/dist/{audio-C4kiPbrV.mjs → audio-DvL7gQQ8.mjs} +3 -3
  3. package/dist/{audio-C4kiPbrV.mjs.map → audio-DvL7gQQ8.mjs.map} +1 -1
  4. package/dist/{audio-CiXUMGyk.mjs → audio-sZdRh_qg.mjs} +3 -3
  5. package/dist/{audio-CiXUMGyk.mjs.map → audio-sZdRh_qg.mjs.map} +1 -1
  6. package/dist/{dist-C37594c6.mjs → dist-8vbhZYqX.mjs} +471 -152
  7. package/dist/dist-8vbhZYqX.mjs.map +1 -0
  8. package/dist/{dist-CMF9eNLB.mjs → dist-B0vCkDzJ.mjs} +37 -3
  9. package/dist/dist-B0vCkDzJ.mjs.map +1 -0
  10. package/dist/effects/index.mjs +2 -2
  11. package/dist/{effects-C9connag.mjs → effects-C3nV-k4T.mjs} +4 -4
  12. package/dist/{effects-C9connag.mjs.map → effects-C3nV-k4T.mjs.map} +1 -1
  13. package/dist/fonts/index.mjs +1 -1
  14. package/dist/{fonts-Dn18r_ST.mjs → fonts-dn-pl4Pe.mjs} +3 -3
  15. package/dist/{fonts-Dn18r_ST.mjs.map → fonts-dn-pl4Pe.mjs.map} +1 -1
  16. package/dist/{index-YZsTjqut.d.mts → index-D-O2kwLx.d.mts} +3 -3
  17. package/dist/{index-YZsTjqut.d.mts.map → index-D-O2kwLx.d.mts.map} +1 -1
  18. package/dist/index.d.mts +67 -49
  19. package/dist/index.d.mts.map +1 -1
  20. package/dist/index.mjs +96 -52
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/react/index.mjs +2 -2
  23. package/dist/renderer/index.d.mts +2 -2
  24. package/dist/renderer/index.mjs +3 -3
  25. package/dist/{renderer-DgY7XBYE.mjs → renderer-CMDIVxhv.mjs} +315 -12
  26. package/dist/renderer-CMDIVxhv.mjs.map +1 -0
  27. package/dist/{renderers-CLJQRPVU-CMZnHNK0.mjs → renderers-CLJQRPVU-DuQ7-PaC.mjs} +3 -3
  28. package/dist/{renderers-CLJQRPVU-CMZnHNK0.mjs.map → renderers-CLJQRPVU-DuQ7-PaC.mjs.map} +1 -1
  29. package/dist/signals/index.mjs +5 -5
  30. package/dist/{signals-BB_kdndF.mjs → signals-CLaCq092.mjs} +2 -2
  31. package/dist/{signals-BB_kdndF.mjs.map → signals-CLaCq092.mjs.map} +1 -1
  32. package/package.json +52 -52
  33. package/dist/dist-C37594c6.mjs.map +0 -1
  34. package/dist/dist-CMF9eNLB.mjs.map +0 -1
  35. package/dist/renderer-DgY7XBYE.mjs.map +0 -1
@@ -5234,6 +5234,8 @@ var SlugGenerator = class {
5234
5234
  return buffer;
5235
5235
  }
5236
5236
  };
5237
+ /** Per-font load timings are debug output: shown only with LOG_LEVEL=debug or trace. */
5238
+ const VERBOSE_FONT_LOGS = typeof process !== "undefined" && /^(debug|trace)$/i.test(process.env?.LOG_LEVEL ?? "");
5237
5239
  function parseFontWeight(fontWeight) {
5238
5240
  if (!fontWeight) return 400;
5239
5241
  if (fontWeight === "bold") return 700;
@@ -5307,6 +5309,10 @@ var SlugFontCache = class SlugFontCache$1 {
5307
5309
  static get parsedFontCache() {
5308
5310
  return g[GLOBAL_PARSED_KEY];
5309
5311
  }
5312
+ /** Register an already parsed font (e.g. by FontManager) under a family name. */
5313
+ static setParsed(fontFamily, font) {
5314
+ SlugFontCache$1.parsedFontCache.set(fontFamily, font);
5315
+ }
5310
5316
  static getParsed(fontFamily) {
5311
5317
  return SlugFontCache$1.parsedFontCache.get(fontFamily) || null;
5312
5318
  }
@@ -5693,7 +5699,7 @@ var SlugFontCache = class SlugFontCache$1 {
5693
5699
  whitelist: whitelist || null
5694
5700
  }).generate(device, font);
5695
5701
  const generateTime = performance.now();
5696
- console.log(`[SlugFontCache] Preloaded and parsed font "${fontFamily}":\n - fontkit parse: ${(parseTime - startTime).toFixed(2)}ms\n - slug generation: ${(generateTime - parseTime).toFixed(2)}ms\n - total: ${(generateTime - startTime).toFixed(2)}ms`);
5702
+ if (VERBOSE_FONT_LOGS) console.log(`[SlugFontCache] Preloaded and parsed font "${fontFamily}":\n - fontkit parse: ${(parseTime - startTime).toFixed(2)}ms\n - slug generation: ${(generateTime - parseTime).toFixed(2)}ms\n - total: ${(generateTime - startTime).toFixed(2)}ms`);
5697
5703
  generated.fontFamily = fontFamily;
5698
5704
  SlugFontCache$1.cache.set(fontFamily, generated);
5699
5705
  SlugFontCache$1.listeners.forEach((l) => {
@@ -6622,10 +6628,11 @@ function getKernOffset(font, a, b) {
6622
6628
  let v = 0;
6623
6629
  if (src) try {
6624
6630
  const s = String.fromCodePoint(a) + String.fromCodePoint(b);
6625
- const laid = src.layout(s);
6631
+ const laid = src.layout?.(s);
6632
+ if (!laid) throw new Error("font cannot shape text");
6626
6633
  const total = laid.positions.reduce((acc, p) => acc + p.xAdvance, 0);
6627
- const advA = src.glyphForCodePoint(a)?.advanceWidth ?? 0;
6628
- const advB = src.glyphForCodePoint(b)?.advanceWidth ?? 0;
6634
+ const advA = src.glyphForCodePoint?.(a)?.advanceWidth ?? 0;
6635
+ const advB = src.glyphForCodePoint?.(b)?.advanceWidth ?? 0;
6629
6636
  v = laid.glyphs.length === 2 ? total - (advA + advB) : 0;
6630
6637
  } catch {
6631
6638
  v = 0;
@@ -11448,6 +11455,7 @@ const ExtendedGlyphPropertiesSchema = object({
11448
11455
  rotationX: number().optional(),
11449
11456
  rotationY: number().optional(),
11450
11457
  rotationZ: number().optional(),
11458
+ formation: VolumetricFormationConfigSchema.optional(),
11451
11459
  skew: number().optional(),
11452
11460
  skewAxis: number().optional(),
11453
11461
  tracking: number().optional(),
@@ -12723,7 +12731,7 @@ function drawParagraphNode(ctx, pass, props) {
12723
12731
  if (slugFont && hasStroke && ctx.device) {
12724
12732
  const alignMultiplier = props.strokeAlign === "outside" ? 2 : 1;
12725
12733
  const strokeWidthInFontUnits = (props.strokeWidth ?? 0) * alignMultiplier * (slugFont.unitsPerEm / fontSize);
12726
- strokeFont = SlugFontCache.getFont(fontFamily, props.fontWeight, ctx.device, strokeWidthInFontUnits, fontSize, props.variableAxes);
12734
+ strokeFont = SlugFontCache.getFont(fontFamily, props.fontWeight, ctx.device, strokeWidthInFontUnits, fontSize, props.variableAxes) ?? null;
12727
12735
  }
12728
12736
  if (!slugFont?.curvesTex || hasStroke && !strokeFont?.curvesTex) {
12729
12737
  const fontUrl = GetFontAssetUrl(fontFamily);
@@ -12762,6 +12770,12 @@ function drawParagraphNode(ctx, pass, props) {
12762
12770
  const alignWidth = props.isCaption ? props.dstRect.width : maxWidth;
12763
12771
  const animApplyBy = props.animation?.applyBy ?? "word";
12764
12772
  const layout = align === "left" && (alignWidth === void 0 || alignWidth === maxWidth) && !pathOptions && animApplyBy === "word" && Boolean(measured?.layout) && measured.layout ? measured.layout : SlugGeometry.layout(textToRender, slugFont, fontSize, letterSpacing, lineHeight, maxWidth, align, animApplyBy, alignWidth, pathOptions, props.spans);
12773
+ const vAlign = props.verticalAlign ?? "top";
12774
+ const slack = props.height != null && !pathOptions && !props.isCaption ? props.height - (measured.height + padding * 2) : 0;
12775
+ const textRect = {
12776
+ ...props.dstRect,
12777
+ y: props.dstRect.y + (vAlign === "middle" ? slack / 2 : vAlign === "bottom" ? slack : 0)
12778
+ };
12765
12779
  const anim = props.animation;
12766
12780
  const isBold = props.fontWeight === "bold" || typeof props.fontWeight === "number" && props.fontWeight >= 600 || typeof props.fontWeight === "string" && parseInt(props.fontWeight, 10) >= 600;
12767
12781
  const hasTrueWeight = "hasTrueWeight" in slugFont && Boolean(slugFont.hasTrueWeight);
@@ -12829,7 +12843,7 @@ function drawParagraphNode(ctx, pass, props) {
12829
12843
  ctx.renderer.drawRRect(pass, {
12830
12844
  rect: {
12831
12845
  x: props.dstRect.x + padding + m.x,
12832
- y: props.dstRect.y + padding + m.y,
12846
+ y: textRect.y + padding + m.y,
12833
12847
  width: m.width,
12834
12848
  height: m.height
12835
12849
  },
@@ -13001,10 +13015,10 @@ function drawParagraphNode(ctx, pass, props) {
13001
13015
  const rotatedR0x = r0x * cosT - r0y * sinT;
13002
13016
  const rotatedR0y = r0x * sinT + r0y * cosT;
13003
13017
  biasX = glyphX + rotatedR0x + props.dstRect.x + padding + offsetX + boldShift;
13004
- biasY = glyph.y + rotatedR0y + props.dstRect.y + padding + offsetY;
13018
+ biasY = glyph.y + rotatedR0y + textRect.y + padding + offsetY;
13005
13019
  } else {
13006
13020
  biasX = glyphX + cp.bearingX * fontScale + scaleX + props.dstRect.x + padding + offsetX + boldShift;
13007
- biasY = glyph.y - cp.bearingY * fontScale + scaleY + props.dstRect.y + padding + offsetY;
13021
+ biasY = glyph.y - cp.bearingY * fontScale + scaleY + textRect.y + padding + offsetY;
13008
13022
  }
13009
13023
  if (anchorConfig) {
13010
13024
  const pivot = computeAnchorPivotOffset({
@@ -13210,7 +13224,7 @@ function drawParagraphNode(ctx, pass, props) {
13210
13224
  const offsetAdjustment = (localEmojiSize - emoji$1.size) / 2;
13211
13225
  const emojiDstRect = {
13212
13226
  x: props.dstRect.x + emoji$1.x + padding - offsetAdjustment,
13213
- y: props.dstRect.y + emoji$1.y + padding - offsetAdjustment,
13227
+ y: textRect.y + emoji$1.y + padding - offsetAdjustment,
13214
13228
  width: localEmojiSize,
13215
13229
  height: localEmojiSize
13216
13230
  };
@@ -13239,13 +13253,13 @@ function drawParagraphNode(ctx, pass, props) {
13239
13253
  const cursorWidth = cursorCfg.width ?? 2.5;
13240
13254
  const cursorSpacing = cursorCfg.spacing ?? 3;
13241
13255
  let cursorX = props.dstRect.x + padding;
13242
- let cursorY = props.dstRect.y + padding;
13256
+ let cursorY = textRect.y + padding;
13243
13257
  if (layout.glyphs.length > 0) {
13244
13258
  const lastG = layout.glyphs[layout.glyphs.length - 1];
13245
13259
  const fontScale = fontSize / slugFont.unitsPerEm;
13246
13260
  const adv = (lastG.cp?.advanceWidth || 0) * fontScale;
13247
13261
  cursorX = lastG.x + props.dstRect.x + padding + adv + cursorSpacing;
13248
- cursorY = lastG.y + props.dstRect.y + padding;
13262
+ cursorY = lastG.y + textRect.y + padding;
13249
13263
  }
13250
13264
  if (cursorCfg.style === "underscore") ctx.renderer.drawRect(pass, {
13251
13265
  x: cursorX,
@@ -15339,6 +15353,15 @@ const clearVideoNodeState = (nodeId, device) => {
15339
15353
  lastSourceUrl.delete(key);
15340
15354
  }
15341
15355
  };
15356
+ const clearAllVideoCache = (device) => {
15357
+ for (const key of lastGoodFrameKey.values()) {
15358
+ textureCache.evict(key, device);
15359
+ textureCache.activeKeys.delete(key);
15360
+ }
15361
+ lastGoodFrameKey.clear();
15362
+ lastSourceUrl.clear();
15363
+ pendingDecodes.clear();
15364
+ };
15342
15365
  var BufferPool = class {
15343
15366
  buffers = [];
15344
15367
  index = 0;
@@ -19963,7 +19986,7 @@ var Path3D = class Path3D$1 {
19963
19986
  /**
19964
19987
  * Extracts pitch, yaw, roll Euler angles in degrees from the orthonormal frame.
19965
19988
  */
19966
- eulerFromFrame(tangent, normal, binormal) {
19989
+ eulerFromFrame(tangent, normal, _binormal) {
19967
19990
  return [
19968
19991
  Math.asin(Math.max(-1, Math.min(1, -tangent[1]))) * (180 / Math.PI),
19969
19992
  Math.atan2(tangent[0], tangent[2]) * (180 / Math.PI),
@@ -20908,14 +20931,12 @@ var FbxLoader = class FbxLoader$1 {
20908
20931
  if (offset + 25 > u8.byteLength) return null;
20909
20932
  endOffset = Number(view.getBigUint64(offset, true));
20910
20933
  numProperties = Number(view.getBigUint64(offset + 8, true));
20911
- Number(view.getBigUint64(offset + 16, true));
20912
20934
  nameLen = view.getUint8(offset + 24);
20913
20935
  offset += 25;
20914
20936
  } else {
20915
20937
  if (offset + 13 > u8.byteLength) return null;
20916
20938
  endOffset = view.getUint32(offset, true);
20917
20939
  numProperties = view.getUint32(offset + 4, true);
20918
- view.getUint32(offset + 8, true);
20919
20940
  nameLen = view.getUint8(offset + 12);
20920
20941
  offset += 13;
20921
20942
  }
@@ -21125,7 +21146,6 @@ var FbxLoader = class FbxLoader$1 {
21125
21146
  const indicesNode = geomNode.children.find((c) => c.name === "PolygonVertexIndex");
21126
21147
  const normalLayer = geomNode.children.find((c) => c.name === "LayerElementNormal");
21127
21148
  const uvLayer = geomNode.children.find((c) => c.name === "LayerElementUV");
21128
- geomNode.children.find((c) => c.name === "LayerElementMaterial");
21129
21149
  if (!verticesNode || !indicesNode) continue;
21130
21150
  const rawVerts = extractArrayProp(verticesNode);
21131
21151
  const rawPolyIndices = extractArrayProp(indicesNode);
@@ -21242,7 +21262,6 @@ var FbxLoader = class FbxLoader$1 {
21242
21262
  const cIndexes = extractArrayProp(indexesNode);
21243
21263
  const cWeights = extractArrayProp(weightsNode);
21244
21264
  for (let k = 0; k < cIndexes.length; k++) {
21245
- cIndexes[k];
21246
21265
  const weight = cWeights[k] ?? 0;
21247
21266
  for (let v = 0; v < numVerts; v++) for (let slot = 0; slot < 4; slot++) if (jointWeights[v * 4 + slot] === 0) {
21248
21267
  jointIndices[v * 4 + slot] = boneIdx;
@@ -21266,7 +21285,7 @@ var FbxLoader = class FbxLoader$1 {
21266
21285
  materialName: entry.name
21267
21286
  });
21268
21287
  }
21269
- for (const [id, entry] of objects) if (entry.node.name === "Material") {
21288
+ for (const [, entry] of objects) if (entry.node.name === "Material") {
21270
21289
  const props70 = entry.node.children.find((c) => c.name === "Properties70");
21271
21290
  let diffuseColor = [
21272
21291
  1,
@@ -21306,37 +21325,48 @@ var FbxLoader = class FbxLoader$1 {
21306
21325
  const layerConn = connections.find((c) => c.parentId === id && objects.get(c.childId)?.node.name === "AnimationLayer");
21307
21326
  if (layerConn) {
21308
21327
  const curveNodeConns = connections.filter((c) => c.parentId === layerConn.childId && objects.get(c.childId)?.node.name === "AnimationCurveNode");
21328
+ const groups = /* @__PURE__ */ new Map();
21309
21329
  for (const cnc of curveNodeConns) {
21310
21330
  const curveNodeObj = objects.get(cnc.childId);
21311
21331
  if (!curveNodeObj) continue;
21312
21332
  const modelConn = connections.find((c) => c.childId === curveNodeObj.id && objects.get(c.parentId)?.node.name === "Model");
21313
- const nodeName = (modelConn ? objects.get(modelConn.parentId) : null)?.name ?? curveNodeObj.name;
21314
- const propType = (modelConn?.property ?? curveNodeObj.name).toLowerCase();
21315
- const animCurves = connections.filter((c) => c.parentId === curveNodeObj.id && objects.get(c.childId)?.node.name === "AnimationCurve");
21316
- for (const ac of animCurves) {
21317
- const curveObj = objects.get(ac.childId)?.node;
21318
- const keyTimeNode = curveObj?.children.find((c) => c.name === "KeyTime");
21319
- const keyValueNode = curveObj?.children.find((c) => c.name === "KeyValueFloat");
21320
- if (keyTimeNode && keyValueNode) {
21321
- const rawTimes = extractArrayProp(keyTimeNode);
21322
- const rawValues = extractArrayProp(keyValueNode);
21323
- const times = new Float32Array(rawTimes.length);
21324
- const values = new Float32Array(rawValues.length);
21325
- for (let k = 0; k < rawTimes.length; k++) {
21326
- times[k] = (rawTimes[k] ?? 0) / FbxLoader$1.FBX_KTIME;
21327
- values[k] = rawValues[k] ?? 0;
21328
- }
21329
- const isRotation = propType.includes("rot") || propType.includes("r");
21330
- const isScaling = propType.includes("sc") || propType.includes("s");
21331
- tracks.push({
21332
- nodeName,
21333
- times,
21334
- translations: !isRotation && !isScaling ? values : void 0,
21335
- rotations: isRotation ? values : void 0,
21336
- scalings: isScaling ? values : void 0
21337
- });
21338
- }
21333
+ const targetModel = modelConn ? objects.get(modelConn.parentId) : void 0;
21334
+ const kind = transformKindFromProperty(modelConn?.propName) ?? transformKindFromProperty(curveNodeObj.name);
21335
+ if (!kind) continue;
21336
+ const key = `${targetModel?.id ?? curveNodeObj.id}|${kind}`;
21337
+ let group = groups.get(key);
21338
+ if (!group) {
21339
+ const model = targetModel?.node;
21340
+ group = {
21341
+ nodeName: targetModel?.name ?? curveNodeObj.name,
21342
+ model,
21343
+ kind,
21344
+ defaults: staticTransform(model, curveNodeObj.node, kind),
21345
+ channels: [
21346
+ void 0,
21347
+ void 0,
21348
+ void 0
21349
+ ]
21350
+ };
21351
+ groups.set(key, group);
21339
21352
  }
21353
+ connections.filter((c) => c.parentId === curveNodeObj.id && objects.get(c.childId)?.node.name === "AnimationCurve").forEach((cc, order) => {
21354
+ const axis = axisFromProperty(cc.propName) ?? order;
21355
+ if (axis > 2) return;
21356
+ const curveObj = objects.get(cc.childId)?.node;
21357
+ const times = extractArrayProp(curveObj?.children.find((c) => c.name === "KeyTime"));
21358
+ const values = extractArrayProp(curveObj?.children.find((c) => c.name === "KeyValueFloat"));
21359
+ const n = Math.min(times.length, values.length);
21360
+ if (n === 0) return;
21361
+ group.channels[axis] = {
21362
+ times: Array.from(times.slice(0, n), (t) => Number(t)),
21363
+ values: Array.from(values.slice(0, n), (v) => Number(v))
21364
+ };
21365
+ });
21366
+ }
21367
+ for (const group of groups.values()) {
21368
+ const track = buildTransformTrack(group);
21369
+ if (track) tracks.push(track);
21340
21370
  }
21341
21371
  }
21342
21372
  let maxDuration = 1;
@@ -21413,6 +21443,151 @@ var FbxLoader = class FbxLoader$1 {
21413
21443
  };
21414
21444
  }
21415
21445
  };
21446
+ /**
21447
+ * Classifies a curve node from its OP connection property ("Lcl Translation",
21448
+ * "Lcl Rotation", "Lcl Scaling") or, failing that, its short name ("T", "R", "S").
21449
+ */
21450
+ function transformKindFromProperty(name) {
21451
+ switch (name) {
21452
+ case "Lcl Translation":
21453
+ case "T": return "T";
21454
+ case "Lcl Rotation":
21455
+ case "R": return "R";
21456
+ case "Lcl Scaling":
21457
+ case "S": return "S";
21458
+ default: return;
21459
+ }
21460
+ }
21461
+ /** Channel index of a curve from its curve-node connection property ("d|X" → 0). */
21462
+ function axisFromProperty(name) {
21463
+ switch (name) {
21464
+ case "d|X": return 0;
21465
+ case "d|Y": return 1;
21466
+ case "d|Z": return 2;
21467
+ default: return;
21468
+ }
21469
+ }
21470
+ /** Numeric values of a Properties70 `P` entry (they start at props[4]). */
21471
+ function readProperty70(node, name) {
21472
+ const p = (node?.children.find((c) => c.name === "Properties70"))?.children.find((c) => c.name === "P" && c.props[0] === name);
21473
+ if (!p) return void 0;
21474
+ return p.props.slice(4).map((v) => Number(v));
21475
+ }
21476
+ /** A model's rest value for one transform, used where a channel has no curve. */
21477
+ function staticTransform(model, curveNode, kind) {
21478
+ const fallback = kind === "S" ? 1 : 0;
21479
+ const modelProp = readProperty70(model, kind === "T" ? "Lcl Translation" : kind === "R" ? "Lcl Rotation" : "Lcl Scaling");
21480
+ const out = [
21481
+ fallback,
21482
+ fallback,
21483
+ fallback
21484
+ ];
21485
+ for (let axis = 0; axis < 3; axis++) {
21486
+ const fromModel = modelProp?.[axis];
21487
+ const fromCurveNode = readProperty70(curveNode, `d|${"XYZ"[axis]}`)?.[0];
21488
+ const value = fromModel ?? fromCurveNode;
21489
+ if (value !== void 0 && Number.isFinite(value)) out[axis] = value;
21490
+ }
21491
+ return out;
21492
+ }
21493
+ /** FBX EOrder enum (Model "RotationOrder"): axes in the order they are applied. */
21494
+ const FBX_ROTATION_ORDERS = [
21495
+ "XYZ",
21496
+ "XZY",
21497
+ "YZX",
21498
+ "YXZ",
21499
+ "ZXY",
21500
+ "ZYX"
21501
+ ];
21502
+ function rotationOrder(model) {
21503
+ return FBX_ROTATION_ORDERS[readProperty70(model, "RotationOrder")?.[0] ?? 0] ?? "XYZ";
21504
+ }
21505
+ /**
21506
+ * Euler degrees → [x, y, z, w] quaternion. FBX applies the axes in `order`
21507
+ * (XYZ: X first, so R = Rz·Ry·Rx).
21508
+ */
21509
+ function fbxEulerToQuat(degrees, order = "XYZ") {
21510
+ let q = [
21511
+ 0,
21512
+ 0,
21513
+ 0,
21514
+ 1
21515
+ ];
21516
+ for (const axisName of order) {
21517
+ const axis = "XYZ".indexOf(axisName);
21518
+ const half = (degrees[axis] ?? 0) * Math.PI / 360;
21519
+ const s = Math.sin(half);
21520
+ const a = [
21521
+ 0,
21522
+ 0,
21523
+ 0,
21524
+ Math.cos(half)
21525
+ ];
21526
+ a[axis] = s;
21527
+ q = [
21528
+ a[3] * q[0] + a[0] * q[3] + a[1] * q[2] - a[2] * q[1],
21529
+ a[3] * q[1] - a[0] * q[2] + a[1] * q[3] + a[2] * q[0],
21530
+ a[3] * q[2] + a[0] * q[1] - a[1] * q[0] + a[2] * q[3],
21531
+ a[3] * q[3] - a[0] * q[0] - a[1] * q[1] - a[2] * q[2]
21532
+ ];
21533
+ }
21534
+ return q;
21535
+ }
21536
+ /** Linear sample of a curve at `t` (KTime ticks), clamped to its ends. */
21537
+ function sampleCurve(curve, t) {
21538
+ const { times, values } = curve;
21539
+ const last = times.length - 1;
21540
+ if (t <= times[0]) return values[0];
21541
+ if (t >= times[last]) return values[last];
21542
+ let k = 0;
21543
+ while (k < last - 1 && times[k + 1] <= t) k++;
21544
+ const t0 = times[k];
21545
+ const t1 = times[k + 1];
21546
+ const u = t1 > t0 ? (t - t0) / (t1 - t0) : 0;
21547
+ return values[k] + (values[k + 1] - values[k]) * u;
21548
+ }
21549
+ /**
21550
+ * Merges a transform's X/Y/Z curves onto one sorted key set: channels without
21551
+ * a key at some time are interpolated, channels without a curve keep the
21552
+ * static value. Rotations become quaternions in the model's RotationOrder.
21553
+ */
21554
+ function buildTransformTrack(group) {
21555
+ const keySet = /* @__PURE__ */ new Set();
21556
+ for (const channel of group.channels) for (const t of channel?.times ?? []) keySet.add(t);
21557
+ if (keySet.size === 0) return void 0;
21558
+ const keys = [...keySet].sort((a, b) => a - b);
21559
+ const times = new Float32Array(keys.length);
21560
+ const isRotation = group.kind === "R";
21561
+ const values = new Float32Array(keys.length * (isRotation ? 4 : 3));
21562
+ const order = isRotation ? rotationOrder(group.model) : "XYZ";
21563
+ let prev;
21564
+ keys.forEach((key, k) => {
21565
+ times[k] = key / FbxLoader.FBX_KTIME;
21566
+ const v = [
21567
+ 0,
21568
+ 1,
21569
+ 2
21570
+ ].map((axis) => {
21571
+ const channel = group.channels[axis];
21572
+ return channel ? sampleCurve(channel, key) : group.defaults[axis];
21573
+ });
21574
+ if (!isRotation) {
21575
+ values.set(v, k * 3);
21576
+ return;
21577
+ }
21578
+ const q = fbxEulerToQuat(v, order);
21579
+ if (prev && prev[0] * q[0] + prev[1] * q[1] + prev[2] * q[2] + prev[3] * q[3] < 0) for (let i = 0; i < 4; i++) q[i] = -q[i];
21580
+ prev = q;
21581
+ values.set(q, k * 4);
21582
+ });
21583
+ return {
21584
+ nodeName: group.nodeName,
21585
+ times,
21586
+ translations: group.kind === "T" ? values : void 0,
21587
+ rotations: isRotation ? values : void 0,
21588
+ scales: group.kind === "S" ? values : void 0
21589
+ };
21590
+ }
21416
21591
  function parseFBX(bufferOrText, options = {}) {
21417
21592
  return FbxLoader.parse(bufferOrText, options);
21418
21593
  }
@@ -22365,7 +22540,6 @@ var GltfLoader = class GltfLoader$1 {
22365
22540
  static unpackGlb(bytes) {
22366
22541
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
22367
22542
  const magic = view.getUint32(0, true);
22368
- view.getUint32(4, true);
22369
22543
  const totalLength = view.getUint32(8, true);
22370
22544
  if (magic !== GLB_MAGIC) throw new Error("Invalid GLB header: magic mismatch");
22371
22545
  let offset = 12;
@@ -22747,6 +22921,9 @@ var GltfLoader = class GltfLoader$1 {
22747
22921
  function loadGLTF(srcOrData, options = {}) {
22748
22922
  return GltfLoader.load(srcOrData, options);
22749
22923
  }
22924
+ function loadGLB(srcOrData, options = {}) {
22925
+ return GltfLoader.load(srcOrData, options);
22926
+ }
22750
22927
  const DEFAULT_PALETTE = [[
22751
22928
  0,
22752
22929
  0,
@@ -22784,7 +22961,6 @@ var VoxLoader = class VoxLoader$1 {
22784
22961
  while (offset + 12 <= bytes.byteLength) {
22785
22962
  const chunkId = new TextDecoder("ascii").decode(bytes.subarray(offset, offset + 4));
22786
22963
  const contentSize = view.getUint32(offset + 4, true);
22787
- view.getUint32(offset + 8, true);
22788
22964
  offset += 12;
22789
22965
  const chunkDataOffset = offset;
22790
22966
  if (chunkId === "SIZE") {
@@ -22827,7 +23003,7 @@ var VoxLoader = class VoxLoader$1 {
22827
23003
  }
22828
23004
  return VoxLoader$1.buildModel(voxels, palette, sizeX, sizeY, sizeZ, options);
22829
23005
  }
22830
- static buildModel(voxels, palette, sizeX, sizeY, sizeZ, options) {
23006
+ static buildModel(voxels, palette, _sizeX, _sizeY, _sizeZ, options) {
22831
23007
  const voxelSet = /* @__PURE__ */ new Set();
22832
23008
  voxels.forEach((v) => voxelSet.add(`${v.x},${v.y},${v.z}`));
22833
23009
  const rawPositions = [];
@@ -23177,7 +23353,7 @@ var ThreeDSLoader = class ThreeDSLoader$1 {
23177
23353
  offset = nextChunkOffset;
23178
23354
  }
23179
23355
  }
23180
- static readTriMesh(view, startOffset, endOffset, mesh, materials) {
23356
+ static readTriMesh(view, startOffset, endOffset, mesh, _materials) {
23181
23357
  let offset = startOffset;
23182
23358
  while (offset + 6 <= endOffset) {
23183
23359
  const chunkId = view.getUint16(offset, true);
@@ -24359,7 +24535,7 @@ var Mesh3DPipeline = class {
24359
24535
  defaultWhiteTexture;
24360
24536
  defaultSkinningBuffer;
24361
24537
  modelData = new Float32Array(48);
24362
- constructor(device, format, depthFormat = "depth24plus", cameraLayout, lightsLayout) {
24538
+ constructor(device, format, depthFormat = "depth24plus", cameraLayout, lightsLayout, sampleCount = 1) {
24363
24539
  this.device = device;
24364
24540
  this.modelPool = new BufferPool(192, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
24365
24541
  this.skinningPool = new BufferPool(8192, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
@@ -24478,7 +24654,8 @@ var Mesh3DPipeline = class {
24478
24654
  primitive: {
24479
24655
  topology: "triangle-list",
24480
24656
  cullMode: "none"
24481
- }
24657
+ },
24658
+ multisample: { count: sampleCount }
24482
24659
  });
24483
24660
  this.pipelineMrt = device.createRenderPipeline({
24484
24661
  label: "Mesh3DPipelineMrt",
@@ -24511,7 +24688,8 @@ var Mesh3DPipeline = class {
24511
24688
  primitive: {
24512
24689
  topology: "triangle-list",
24513
24690
  cullMode: "none"
24514
- }
24691
+ },
24692
+ multisample: { count: sampleCount }
24515
24693
  });
24516
24694
  this.defaultWhiteTexture = device.createTexture({
24517
24695
  label: "Mesh3DDefaultWhiteTexture",
@@ -25146,7 +25324,7 @@ var Quad3DPipeline = class {
25146
25324
  vertexBuffer;
25147
25325
  indexBuffer;
25148
25326
  modelData = new Float32Array(52);
25149
- constructor(device, format, depthFormat = "depth24plus") {
25327
+ constructor(device, format, depthFormat = "depth24plus", sampleCount = 1) {
25150
25328
  this.device = device;
25151
25329
  this.modelPool = new BufferPool(208, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
25152
25330
  const module = device.createShaderModule({
@@ -25250,7 +25428,8 @@ var Quad3DPipeline = class {
25250
25428
  primitive: {
25251
25429
  topology: "triangle-list",
25252
25430
  cullMode: "none"
25253
- }
25431
+ },
25432
+ multisample: { count: sampleCount }
25254
25433
  });
25255
25434
  this.pipelineMrt = device.createRenderPipeline({
25256
25435
  label: "Quad3DPipelineMrt",
@@ -25283,7 +25462,8 @@ var Quad3DPipeline = class {
25283
25462
  primitive: {
25284
25463
  topology: "triangle-list",
25285
25464
  cullMode: "none"
25286
- }
25465
+ },
25466
+ multisample: { count: sampleCount }
25287
25467
  });
25288
25468
  const vertices = new Float32Array([
25289
25469
  -.5,
@@ -25688,7 +25868,7 @@ var Slug3DPipeline = class {
25688
25868
  instancePool;
25689
25869
  fontBindGroups = /* @__PURE__ */ new WeakMap();
25690
25870
  modelData = new Float32Array(40);
25691
- constructor(device, format, depthFormat, cameraLayout, lightsLayout) {
25871
+ constructor(device, format, depthFormat, cameraLayout, lightsLayout, sampleCount = 1) {
25692
25872
  this.device = device;
25693
25873
  this.modelPool = new BufferPool(160, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
25694
25874
  this.instancePool = new BufferPool(65536, GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST);
@@ -25783,6 +25963,7 @@ var Slug3DPipeline = class {
25783
25963
  topology: "triangle-list",
25784
25964
  cullMode: "none"
25785
25965
  };
25966
+ const multisample = { count: sampleCount };
25786
25967
  this.pipelineSingle = device.createRenderPipeline({
25787
25968
  label: "Slug3DPipelineSingle",
25788
25969
  layout,
@@ -25796,7 +25977,8 @@ var Slug3DPipeline = class {
25796
25977
  }]
25797
25978
  },
25798
25979
  depthStencil,
25799
- primitive
25980
+ primitive,
25981
+ multisample
25800
25982
  });
25801
25983
  this.pipelineMrt = device.createRenderPipeline({
25802
25984
  label: "Slug3DPipelineMrt",
@@ -25811,7 +25993,8 @@ var Slug3DPipeline = class {
25811
25993
  }, { format: "rgba16float" }]
25812
25994
  },
25813
25995
  depthStencil,
25814
- primitive
25996
+ primitive,
25997
+ multisample
25815
25998
  });
25816
25999
  }
25817
26000
  resetPools() {
@@ -25894,6 +26077,8 @@ var Renderer3D = class {
25894
26077
  samplerCache = new SamplerCache();
25895
26078
  depthTexture = null;
25896
26079
  linearDepthTexture = null;
26080
+ msaaColorTexture = null;
26081
+ msaaLinearDepthTexture = null;
25897
26082
  cameraUniformData = new Float32Array(80);
25898
26083
  meshBuffersCache = /* @__PURE__ */ new WeakMap();
25899
26084
  materialTextures = /* @__PURE__ */ new WeakMap();
@@ -25901,12 +26086,19 @@ var Renderer3D = class {
25901
26086
  modelCache = /* @__PURE__ */ new Map();
25902
26087
  lastCameraPose = null;
25903
26088
  lastCameraBuffer = null;
25904
- constructor(device, format) {
26089
+ /**
26090
+ * Samples per pixel in the 3D pass. Mesh silhouettes (extruded text,
26091
+ * models) are rasterized triangles: without MSAA every edge is a hard
26092
+ * stair-step. 4 is the count every WebGPU device supports; 1 turns it off.
26093
+ */
26094
+ sampleCount;
26095
+ constructor(device, format, sampleCount = 4) {
25905
26096
  this.device = device;
25906
26097
  this.format = format;
25907
- this.quad3dPipeline = new Quad3DPipeline(device, format);
25908
- this.slug3dPipeline = new Slug3DPipeline(device, format, "depth24plus", this.quad3dPipeline.cameraLayout, this.quad3dPipeline.lightsLayout);
25909
- this.mesh3dPipeline = new Mesh3DPipeline(device, format, "depth24plus", this.quad3dPipeline.cameraLayout, this.quad3dPipeline.lightsLayout);
26098
+ this.sampleCount = sampleCount;
26099
+ this.quad3dPipeline = new Quad3DPipeline(device, format, "depth24plus", sampleCount);
26100
+ this.slug3dPipeline = new Slug3DPipeline(device, format, "depth24plus", this.quad3dPipeline.cameraLayout, this.quad3dPipeline.lightsLayout, sampleCount);
26101
+ this.mesh3dPipeline = new Mesh3DPipeline(device, format, "depth24plus", this.quad3dPipeline.cameraLayout, this.quad3dPipeline.lightsLayout, sampleCount);
25910
26102
  this.audioMeshDeformPipeline = new AudioMeshDeformPipeline(device);
25911
26103
  this.cameraPool = new BufferPool(320, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
25912
26104
  this.lightsPool = new BufferPool(LIGHT_UNIFORM_SIZE_BYTES, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
@@ -25972,11 +26164,26 @@ var Renderer3D = class {
25972
26164
  height
25973
26165
  },
25974
26166
  format: "depth24plus",
26167
+ sampleCount: this.sampleCount,
25975
26168
  usage: GPUTextureUsage.RENDER_ATTACHMENT
25976
26169
  });
25977
26170
  }
25978
26171
  return this.depthTexture;
25979
26172
  }
26173
+ getOrCreateMsaaTexture(current, label, format, width, height) {
26174
+ if (current && current.width === width && current.height === height) return current;
26175
+ current?.destroy();
26176
+ return this.device.createTexture({
26177
+ label,
26178
+ size: {
26179
+ width,
26180
+ height
26181
+ },
26182
+ format,
26183
+ sampleCount: this.sampleCount,
26184
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
26185
+ });
26186
+ }
25980
26187
  getOrCreateLinearDepthTexture(width, height) {
25981
26188
  if (!this.linearDepthTexture || this.linearDepthTexture.width !== width || this.linearDepthTexture.height !== height) {
25982
26189
  this.linearDepthTexture?.destroy();
@@ -26042,23 +26249,31 @@ var Renderer3D = class {
26042
26249
  const cameraBindGroup = this.createCameraBindGroup(cameraBuffer);
26043
26250
  const lightsBuffer = this.getOrCreateLightsBuffer(lights);
26044
26251
  const lightsBindGroup = this.lightsUniformBindGroupCache.getBindGroup(this.device, this.quad3dPipeline.lightsLayout, lightsBuffer);
26045
- const colorAttachments = [{
26046
- view: colorTargetView,
26047
- loadOp,
26048
- storeOp: "store",
26049
- clearValue: clearColor
26050
- }];
26051
- if (linearDepthTargetView) colorAttachments.push({
26052
- view: linearDepthTargetView,
26252
+ const msaa = this.sampleCount > 1;
26253
+ if (msaa && loadOp === "load") throw new Error("Renderer3D.beginPass: loadOp 'load' needs a Renderer3D built with sampleCount 1");
26254
+ const attach = (view, msaaTex, clearValue, attachmentLoadOp) => msaaTex ? {
26255
+ view: msaaTex.createView(),
26256
+ resolveTarget: view,
26053
26257
  loadOp: "clear",
26258
+ storeOp: "discard",
26259
+ clearValue
26260
+ } : {
26261
+ view,
26262
+ loadOp: attachmentLoadOp,
26054
26263
  storeOp: "store",
26055
- clearValue: {
26264
+ clearValue
26265
+ };
26266
+ if (msaa) this.msaaColorTexture = this.getOrCreateMsaaTexture(this.msaaColorTexture, "Renderer3DMsaaColor", this.format, surfaceWidth, surfaceHeight);
26267
+ const colorAttachments = [attach(colorTargetView, msaa ? this.msaaColorTexture : null, clearColor, loadOp)];
26268
+ if (linearDepthTargetView) {
26269
+ if (msaa) this.msaaLinearDepthTexture = this.getOrCreateMsaaTexture(this.msaaLinearDepthTexture, "Renderer3DMsaaLinearDepth", "rgba16float", surfaceWidth, surfaceHeight);
26270
+ colorAttachments.push(attach(linearDepthTargetView, msaa ? this.msaaLinearDepthTexture : null, {
26056
26271
  r: camera.far,
26057
26272
  g: 0,
26058
26273
  b: 0,
26059
26274
  a: 1
26060
- }
26061
- });
26275
+ }, "clear"));
26276
+ }
26062
26277
  return {
26063
26278
  pass: encoder.beginRenderPass({
26064
26279
  colorAttachments,
@@ -26066,7 +26281,7 @@ var Renderer3D = class {
26066
26281
  view: depthTex.createView(),
26067
26282
  depthClearValue: 1,
26068
26283
  depthLoadOp: "clear",
26069
- depthStoreOp: "store"
26284
+ depthStoreOp: msaa ? "discard" : "store"
26070
26285
  }
26071
26286
  }),
26072
26287
  cameraBindGroup,
@@ -26438,6 +26653,8 @@ var Renderer3D = class {
26438
26653
  destroy() {
26439
26654
  this.depthTexture?.destroy();
26440
26655
  this.linearDepthTexture?.destroy();
26656
+ this.msaaColorTexture?.destroy();
26657
+ this.msaaLinearDepthTexture?.destroy();
26441
26658
  this.quad3dPipeline.destroy();
26442
26659
  this.slug3dPipeline.destroy();
26443
26660
  this.mesh3dPipeline.destroy();
@@ -26489,13 +26706,11 @@ function loadModel3D(srcOrData, format = "auto", options = {}) {
26489
26706
  * Directional and Point Light Shadow Map Generator with Percentage Closer Filtering (PCF).
26490
26707
  */
26491
26708
  var ShadowPipeline = class {
26492
- device;
26493
26709
  shadowDepthTexture;
26494
26710
  shadowDepthView;
26495
26711
  shadowComparisonSampler;
26496
26712
  lightViewProjMatrix;
26497
26713
  constructor(device, resolution = 2048) {
26498
- this.device = device;
26499
26714
  this.lightViewProjMatrix = Matrix4Math.identity();
26500
26715
  this.shadowDepthTexture = device.createTexture({
26501
26716
  label: "shadow-depth-map",
@@ -26522,7 +26737,7 @@ var ShadowPipeline = class {
26522
26737
  0
26523
26738
  ]);
26524
26739
  const half = orthoSize * .5;
26525
- const projMatrix = Matrix4Math.ortho(-half, half, -half, half, near, far);
26740
+ const projMatrix = Matrix4Math.orthographic(-half, half, -half, half, near, far);
26526
26741
  this.lightViewProjMatrix = Matrix4Math.multiply(projMatrix, viewMatrix);
26527
26742
  return this.lightViewProjMatrix;
26528
26743
  }
@@ -26703,7 +26918,7 @@ var SSAOPipeline = class {
26703
26918
  });
26704
26919
  }
26705
26920
  render(commandEncoder, depthView, normalView, rawAOView, finalAOView, options) {
26706
- const invProj = options.invProjMatrix ?? Matrix4Math.invert(options.projMatrix);
26921
+ const invProj = options.invProjMatrix ?? Matrix4Math.invert(options.projMatrix) ?? Matrix4Math.identity();
26707
26922
  const uBuf = new Float32Array(64);
26708
26923
  uBuf.set(options.projMatrix, 0);
26709
26924
  uBuf.set(invProj, 16);
@@ -26878,7 +27093,19 @@ function isPointInsidePolygon(p, poly) {
26878
27093
  }
26879
27094
  return inside;
26880
27095
  }
26881
- function processCurveQuadratic(p0, cp, end, segments, out) {
27096
+ const DEFAULT_CURVE_TOLERANCE = .1;
27097
+ const MAX_CURVE_SEGMENTS = 64;
27098
+ /**
27099
+ * Segments needed for a polyline through n uniform steps of a curve to stay
27100
+ * within `tolerance`: the chord error is at most max|B''| / (8 n^2).
27101
+ */
27102
+ function curveSegmentCount(maxSecondDerivative, flattening) {
27103
+ if (flattening.segments !== void 0) return flattening.segments;
27104
+ const n = Math.ceil(Math.sqrt(maxSecondDerivative / (8 * flattening.tolerance)));
27105
+ return Math.min(MAX_CURVE_SEGMENTS, Math.max(1, n));
27106
+ }
27107
+ function processCurveQuadratic(p0, cp, end, flattening, out) {
27108
+ const segments = curveSegmentCount(2 * Math.hypot(p0.x - 2 * cp.x + end.x, p0.y - 2 * cp.y + end.y), flattening);
26882
27109
  for (let s = 1; s <= segments; s++) {
26883
27110
  const t = s / segments;
26884
27111
  const mt = 1 - t;
@@ -26888,7 +27115,8 @@ function processCurveQuadratic(p0, cp, end, segments, out) {
26888
27115
  });
26889
27116
  }
26890
27117
  }
26891
- function processCurveCubic(p0, cp1, cp2, end, segments, out) {
27118
+ function processCurveCubic(p0, cp1, cp2, end, flattening, out) {
27119
+ const segments = curveSegmentCount(6 * Math.max(Math.hypot(p0.x - 2 * cp1.x + cp2.x, p0.y - 2 * cp1.y + cp2.y), Math.hypot(cp1.x - 2 * cp2.x + end.x, cp1.y - 2 * cp2.y + end.y)), flattening);
26892
27120
  for (let s = 1; s <= segments; s++) {
26893
27121
  const t = s / segments;
26894
27122
  const mt = 1 - t;
@@ -26898,7 +27126,7 @@ function processCurveCubic(p0, cp1, cp2, end, segments, out) {
26898
27126
  });
26899
27127
  }
26900
27128
  }
26901
- function extractGlyphContours(glyph, glyphX, glyphY, scale, curveSegments) {
27129
+ function extractGlyphContours(glyph, glyphX, glyphY, scale, flattening) {
26902
27130
  const svg = glyph.path?.toSVG();
26903
27131
  if (!svg) return [];
26904
27132
  let edges;
@@ -26947,7 +27175,7 @@ function extractGlyphContours(glyph, glyphX, glyphY, scale, curveSegments) {
26947
27175
  x: (glyphX + args[2]) * scale,
26948
27176
  y: -(glyphY + args[3]) * scale
26949
27177
  };
26950
- processCurveQuadratic(curr, cp, end, curveSegments, current);
27178
+ processCurveQuadratic(curr, cp, end, flattening, current);
26951
27179
  curr = end;
26952
27180
  } else if (name === "bezierCurveTo") {
26953
27181
  const cp1 = {
@@ -26962,45 +27190,90 @@ function extractGlyphContours(glyph, glyphX, glyphY, scale, curveSegments) {
26962
27190
  x: (glyphX + args[4]) * scale,
26963
27191
  y: -(glyphY + args[5]) * scale
26964
27192
  };
26965
- processCurveCubic(curr, cp1, cp2, end, curveSegments, current);
27193
+ processCurveCubic(curr, cp1, cp2, end, flattening, current);
26966
27194
  curr = end;
26967
27195
  } else if (name === "closePath") finishCurrent();
26968
27196
  }
26969
27197
  finishCurrent();
26970
27198
  return contours;
26971
27199
  }
27200
+ /** Side-wall segments meeting at more than this angle (30°) keep a hard edge. */
27201
+ const SIDE_CREASE_COS = Math.cos(30 * Math.PI / 180);
27202
+ /**
27203
+ * Group a glyph's contours into filled outlines and their holes by nesting
27204
+ * depth: a contour inside an even number of others is an outline (the ring
27205
+ * of "O", or the "R" sitting in the counter of "®"), one inside an odd number
27206
+ * is a hole of the smallest outline around it.
27207
+ */
26972
27208
  function organizeGlyphPolygons(glyphContours) {
26973
- if (glyphContours.length === 0) return [];
26974
- const sorted = [...glyphContours].sort((a, b) => Math.abs(computeContourSignedArea(b)) - Math.abs(computeContourSignedArea(a)));
26975
- const outers = [];
26976
- for (const contour of sorted) {
26977
- const area = Math.abs(computeContourSignedArea(contour));
26978
- let parent = null;
26979
- for (const out of outers) if (area < Math.abs(computeContourSignedArea(out.outer))) {
26980
- const sample = contour[0];
26981
- const mid = {
26982
- x: (contour[0].x + contour[Math.floor(contour.length / 2)].x) / 2,
26983
- y: (contour[0].y + contour[Math.floor(contour.length / 2)].y) / 2
26984
- };
26985
- if (isPointInsidePolygon(sample, out.outer) || isPointInsidePolygon(mid, out.outer)) {
26986
- parent = out;
26987
- break;
26988
- }
27209
+ const contours = glyphContours.map((contour) => ({
27210
+ contour,
27211
+ area: Math.abs(computeContourSignedArea(contour))
27212
+ }));
27213
+ contours.sort((a, b) => b.area - a.area);
27214
+ const containers = contours.map(() => []);
27215
+ for (let i = 0; i < contours.length; i++) {
27216
+ const inner = contours[i].contour;
27217
+ const mid = inner[Math.floor(inner.length / 2)];
27218
+ for (let j = 0; j < i; j++) {
27219
+ if (contours[j].area <= contours[i].area) continue;
27220
+ const outer = contours[j].contour;
27221
+ if (isPointInsidePolygon(inner[0], outer) || isPointInsidePolygon(mid, outer)) containers[i].push(j);
26989
27222
  }
26990
- if (parent) parent.holes.push(contour);
26991
- else outers.push({
26992
- outer: contour,
26993
- holes: []
26994
- });
26995
27223
  }
26996
- return outers;
27224
+ const polygons = [];
27225
+ const polygonOf = /* @__PURE__ */ new Map();
27226
+ for (let i = 0; i < contours.length; i++) {
27227
+ const around = containers[i];
27228
+ if (around.length % 2 === 0) {
27229
+ const poly = {
27230
+ outer: contours[i].contour,
27231
+ holes: []
27232
+ };
27233
+ polygons.push(poly);
27234
+ polygonOf.set(i, poly);
27235
+ } else polygonOf.get(around[around.length - 1])?.holes.push(contours[i].contour);
27236
+ }
27237
+ return polygons;
27238
+ }
27239
+ /**
27240
+ * A glyph's outlines are flattened and sorted into outlines and holes once per
27241
+ * font, size and flattening, at the glyph origin; each use gets a translated
27242
+ * copy (callers reorder and move the points they are given).
27243
+ */
27244
+ const glyphOutlineCache = /* @__PURE__ */ new WeakMap();
27245
+ function glyphPolygonsAt(font, glyph, glyphX, glyphY, scale, flattening) {
27246
+ let byGlyph = glyphOutlineCache.get(font);
27247
+ if (!byGlyph) {
27248
+ byGlyph = /* @__PURE__ */ new Map();
27249
+ glyphOutlineCache.set(font, byGlyph);
27250
+ }
27251
+ const key = `${glyph.id}|${scale}|${flattening.segments ?? ""}|${flattening.tolerance}`;
27252
+ let atOrigin = byGlyph.get(key);
27253
+ if (!atOrigin) {
27254
+ atOrigin = organizeGlyphPolygons(extractGlyphContours(glyph, 0, 0, scale, flattening));
27255
+ byGlyph.set(key, atOrigin);
27256
+ }
27257
+ const dx = glyphX * scale;
27258
+ const dy = -glyphY * scale;
27259
+ const moved = (c) => c.map((p) => ({
27260
+ x: p.x + dx,
27261
+ y: p.y + dy
27262
+ }));
27263
+ return atOrigin.map((poly) => ({
27264
+ outer: moved(poly.outer),
27265
+ holes: poly.holes.map(moved)
27266
+ }));
26997
27267
  }
26998
27268
  function generateExtrudedTextGeometry(options) {
26999
27269
  const font = options.font;
27000
27270
  const fontSize = options.fontSize ?? 48;
27001
27271
  const depth = options.depth ?? 20;
27002
27272
  const halfDepth = depth / 2;
27003
- const curveSegments = Math.max(2, options.curveSegments ?? 5);
27273
+ const flattening = {
27274
+ segments: options.curveSegments !== void 0 ? Math.max(2, options.curveSegments) : void 0,
27275
+ tolerance: Math.max(.001, options.curveTolerance ?? DEFAULT_CURVE_TOLERANCE)
27276
+ };
27004
27277
  const scale = fontSize / (font.unitsPerEm || 1e3);
27005
27278
  const run = font.layout(options.text);
27006
27279
  let cursorX = 0;
@@ -27008,7 +27281,7 @@ function generateExtrudedTextGeometry(options) {
27008
27281
  for (let i = 0; i < run.glyphs.length; i++) {
27009
27282
  const glyph = run.glyphs[i];
27010
27283
  const pos = run.positions[i];
27011
- const glyphPolys = organizeGlyphPolygons(extractGlyphContours(glyph, cursorX + (pos.xOffset ?? 0), pos.yOffset ?? 0, scale, curveSegments));
27284
+ const glyphPolys = glyphPolygonsAt(font, glyph, cursorX + (pos.xOffset ?? 0), pos.yOffset ?? 0, scale, flattening);
27012
27285
  for (const gp of glyphPolys) polygons.push(gp);
27013
27286
  cursorX += (pos.xAdvance ?? glyph.advanceWidth ?? 0) + (options.letterSpacing ? options.letterSpacing / scale : 0);
27014
27287
  }
@@ -27039,8 +27312,8 @@ function generateExtrudedTextGeometry(options) {
27039
27312
  const w = maxX - minX;
27040
27313
  const h = maxY - minY;
27041
27314
  let alignOffsetX = (minX + maxX) / 2;
27042
- if (options.align === "left") alignOffsetX = minX;
27043
- if (options.align === "right") alignOffsetX = maxX;
27315
+ if (options.align === "left" || options.align === "start") alignOffsetX = minX;
27316
+ if (options.align === "right" || options.align === "end") alignOffsetX = maxX;
27044
27317
  let alignOffsetY = (minY + maxY) / 2;
27045
27318
  if (options.verticalAlign === "top") alignOffsetY = minY;
27046
27319
  if (options.verticalAlign === "bottom") alignOffsetY = maxY;
@@ -27054,6 +27327,10 @@ function generateExtrudedTextGeometry(options) {
27054
27327
  p.y -= alignOffsetY;
27055
27328
  }
27056
27329
  }
27330
+ minX -= alignOffsetX;
27331
+ maxX -= alignOffsetX;
27332
+ minY -= alignOffsetY;
27333
+ maxY -= alignOffsetY;
27057
27334
  const frontPositions = [];
27058
27335
  const frontNormals = [];
27059
27336
  const frontUvs = [];
@@ -27062,28 +27339,57 @@ function generateExtrudedTextGeometry(options) {
27062
27339
  const sideNormals = [];
27063
27340
  const sideUvs = [];
27064
27341
  const sideIndices = [];
27065
- const addSideQuad = (p0, p1) => {
27066
- const dx = p1.x - p0.x;
27067
- const dy = p1.y - p0.y;
27068
- const len = Math.hypot(dx, dy);
27069
- if (len < 1e-6) return;
27070
- const nx = dy / len;
27071
- const ny = -dx / len;
27072
- const baseIdx = sidePositions.length / 3;
27073
- sidePositions.push(p0.x, p0.y, halfDepth);
27074
- sideNormals.push(nx, ny, 0);
27075
- sideUvs.push(0, 0);
27076
- sidePositions.push(p1.x, p1.y, halfDepth);
27077
- sideNormals.push(nx, ny, 0);
27078
- sideUvs.push(1, 0);
27079
- sidePositions.push(p1.x, p1.y, -halfDepth);
27080
- sideNormals.push(nx, ny, 0);
27081
- sideUvs.push(1, 1);
27082
- sidePositions.push(p0.x, p0.y, -halfDepth);
27083
- sideNormals.push(nx, ny, 0);
27084
- sideUvs.push(0, 1);
27085
- sideIndices.push(baseIdx, baseIdx + 2, baseIdx + 1);
27086
- sideIndices.push(baseIdx, baseIdx + 3, baseIdx + 2);
27342
+ const addColumn = (p, n, u) => {
27343
+ const index = sidePositions.length / 3;
27344
+ sidePositions.push(p.x, p.y, halfDepth, p.x, p.y, -halfDepth);
27345
+ sideNormals.push(n.x, n.y, 0, n.x, n.y, 0);
27346
+ sideUvs.push(u, 0, u, 1);
27347
+ return index;
27348
+ };
27349
+ const addSideWall = (contour) => {
27350
+ const pts = contour.filter((p, k) => {
27351
+ const next = contour[(k + 1) % contour.length];
27352
+ return Math.hypot(next.x - p.x, next.y - p.y) >= 1e-6;
27353
+ });
27354
+ const count = pts.length;
27355
+ if (count < 2) return;
27356
+ const edgeLengths = [];
27357
+ const faceNormals = pts.map((p, k) => {
27358
+ const next = pts[(k + 1) % count];
27359
+ const len = Math.hypot(next.x - p.x, next.y - p.y);
27360
+ edgeLengths.push(len);
27361
+ return {
27362
+ x: (next.y - p.y) / len,
27363
+ y: -(next.x - p.x) / len
27364
+ };
27365
+ });
27366
+ const perimeter = edgeLengths.reduce((a, b) => a + b, 0);
27367
+ const startColumn = [];
27368
+ const endColumn = [];
27369
+ let along = 0;
27370
+ for (let k = 0; k < count; k++) {
27371
+ const incoming = faceNormals[(k - 1 + count) % count];
27372
+ const outgoing = faceNormals[k];
27373
+ const u = along / perimeter;
27374
+ const sx = incoming.x + outgoing.x;
27375
+ const sy = incoming.y + outgoing.y;
27376
+ const averaged = incoming.x * outgoing.x + incoming.y * outgoing.y >= SIDE_CREASE_COS ? {
27377
+ x: sx / Math.hypot(sx, sy),
27378
+ y: sy / Math.hypot(sx, sy)
27379
+ } : null;
27380
+ if (averaged && k > 0) startColumn[k] = endColumn[k] = addColumn(pts[k], averaged, u);
27381
+ else {
27382
+ endColumn[k] = addColumn(pts[k], averaged ?? incoming, k === 0 ? 1 : u);
27383
+ startColumn[k] = addColumn(pts[k], averaged ?? outgoing, u);
27384
+ }
27385
+ along += edgeLengths[k];
27386
+ }
27387
+ for (let k = 0; k < count; k++) {
27388
+ const a = startColumn[k];
27389
+ const b = endColumn[(k + 1) % count];
27390
+ sideIndices.push(a, b + 1, b);
27391
+ sideIndices.push(a, a + 1, b + 1);
27392
+ }
27087
27393
  };
27088
27394
  for (const poly of polygons) {
27089
27395
  const outer = poly.outer;
@@ -27102,28 +27408,22 @@ function generateExtrudedTextGeometry(options) {
27102
27408
  for (let v = 0; v < vertCount; v++) {
27103
27409
  const vx = earcutCoords[v * 2];
27104
27410
  const vy = earcutCoords[v * 2 + 1];
27105
- frontPositions.push(vx, vy, halfDepth);
27106
- frontNormals.push(0, 0, 1);
27411
+ frontPositions.push(vx, vy, -halfDepth);
27412
+ frontNormals.push(0, 0, -1);
27107
27413
  frontUvs.push((vx - minX) / (w || 1), (vy - minY) / (h || 1));
27108
27414
  }
27109
- for (let t = 0; t < triangles.length; t += 3) frontIndices.push(frontBaseIdx + triangles[t], frontBaseIdx + triangles[t + 1], frontBaseIdx + triangles[t + 2]);
27110
- const backBaseIdx = sidePositions.length / 3;
27415
+ for (let t = 0; t < triangles.length; t += 3) frontIndices.push(frontBaseIdx + triangles[t], frontBaseIdx + triangles[t + 2], frontBaseIdx + triangles[t + 1]);
27416
+ const backBaseIdx = frontPositions.length / 3;
27111
27417
  for (let v = 0; v < vertCount; v++) {
27112
27418
  const vx = earcutCoords[v * 2];
27113
27419
  const vy = earcutCoords[v * 2 + 1];
27114
- sidePositions.push(vx, vy, -halfDepth);
27115
- sideNormals.push(0, 0, -1);
27116
- sideUvs.push((vx - minX) / (w || 1), (vy - minY) / (h || 1));
27117
- }
27118
- for (let t = 0; t < triangles.length; t += 3) sideIndices.push(backBaseIdx + triangles[t], backBaseIdx + triangles[t + 2], backBaseIdx + triangles[t + 1]);
27119
- for (let k = 0; k < outer.length; k++) {
27120
- const next = (k + 1) % outer.length;
27121
- addSideQuad(outer[k], outer[next]);
27122
- }
27123
- for (const hole of poly.holes) for (let k = 0; k < hole.length; k++) {
27124
- const next = (k + 1) % hole.length;
27125
- addSideQuad(hole[k], hole[next]);
27420
+ frontPositions.push(vx, vy, halfDepth);
27421
+ frontNormals.push(0, 0, 1);
27422
+ frontUvs.push((vx - minX) / (w || 1), (vy - minY) / (h || 1));
27126
27423
  }
27424
+ for (let t = 0; t < triangles.length; t += 3) frontIndices.push(backBaseIdx + triangles[t], backBaseIdx + triangles[t + 1], backBaseIdx + triangles[t + 2]);
27425
+ addSideWall(outer);
27426
+ for (const hole of poly.holes) addSideWall(hole);
27127
27427
  }
27128
27428
  const frontColor = parseHexOrRgbColor(options.fill ?? "#ffffff");
27129
27429
  const sideColor = parseHexOrRgbColor(options.bevelColor ?? options.fill ?? "#ffffff");
@@ -27133,8 +27433,6 @@ function generateExtrudedTextGeometry(options) {
27133
27433
  shading: options.material ?? "lit",
27134
27434
  shininess: options.shininess ?? 32,
27135
27435
  roughness: options.roughness,
27136
- specularIntensity: options.specularIntensity ?? .8,
27137
- ambientIntensity: options.ambientIntensity ?? 1,
27138
27436
  metallic: options.metallic,
27139
27437
  twoSided: true
27140
27438
  };
@@ -27144,8 +27442,6 @@ function generateExtrudedTextGeometry(options) {
27144
27442
  shading: options.material ?? "lit",
27145
27443
  shininess: options.shininess ?? 32,
27146
27444
  roughness: options.roughness,
27147
- specularIntensity: options.specularIntensity ?? .8,
27148
- ambientIntensity: options.ambientIntensity ?? 1,
27149
27445
  metallic: options.metallic,
27150
27446
  twoSided: true
27151
27447
  };
@@ -27176,7 +27472,30 @@ function generateExtrudedTextGeometry(options) {
27176
27472
  "front-material": frontMaterial,
27177
27473
  "side-material": sideMaterial
27178
27474
  },
27179
- animations: []
27475
+ animations: [],
27476
+ bounds: {
27477
+ min: [
27478
+ minX,
27479
+ minY,
27480
+ -halfDepth
27481
+ ],
27482
+ max: [
27483
+ maxX,
27484
+ maxY,
27485
+ halfDepth
27486
+ ],
27487
+ center: [
27488
+ (minX + maxX) / 2,
27489
+ (minY + maxY) / 2,
27490
+ 0
27491
+ ],
27492
+ size: [
27493
+ w,
27494
+ h,
27495
+ depth
27496
+ ],
27497
+ boundingSphereRadius: Math.hypot(w, h, depth) / 2
27498
+ }
27180
27499
  },
27181
27500
  bounds: {
27182
27501
  minX,
@@ -27191,5 +27510,5 @@ function generateExtrudedTextGeometry(options) {
27191
27510
  }
27192
27511
 
27193
27512
  //#endregion
27194
- export { parseOBJ as $, drawCaptionNode as A, generateExtrudedTextGeometry as B, SlugGeometry as C, chartBridgeCache as D, buildWGSLSignalFn as E, drawParagraphNode as F, loadFBX as G, getRenderer2D as H, drawSignalNode as I, lutStore as J, loadModel3D as K, drawSvgNode as L, drawGifNode as M, drawImageNode as N, clearVideoNodeState as O, drawLottieNode as P, parseMTL as Q, drawVideoNode as R, SlugFontCache as S, WebGPUAudioProcessor as T, initHeadlessWebGPU as U, getHeadlessFontPath as V, inputStore as W, parseColor as X, mediaDecoderCache as Y, parseFBX as Z, Renderer2D as _, Camera3D as a, GetFontAssetUrl as at, ShadowPipeline as b, GlassPipeline as c, resolveMediaSourceUrl as ct, HeadlessGPUCanvasContextMock as d, registerHeadlessFont as et, Light3D as f, RenderProvider as g, Path3D as h, AudioSignalComputePipeline as i, useRenderContext as it, drawChartNode as j, colorMatrixWgsl as k, HeadlessChartCanvasElementMock as l, NodeSurfaceProvider as m, AudioLatentTrackCache as n, signalRegistry as nt, ChartGPUEngineBridge as o, R2_CUSTOM_DOMAIN as ot, MotionBlurPipeline as p, loadOBJ as q, AudioMeshDeformPipeline as r, textureCache as rt, EffectPipeline as s, getEnv as st, AudioLatentEngine as t, shaderStore as tt, HeadlessChartContainerMock as u, Renderer3D as v, SlugPipeline as w, SignalRegistry as x, SSAOPipeline as y, ensureDevice as z };
27195
- //# sourceMappingURL=dist-C37594c6.mjs.map
27513
+ export { parseMTL as $, colorMatrixWgsl as A, ensureDevice as B, SlugGeometry as C, chartBridgeCache as D, buildWGSLSignalFn as E, drawLottieNode as F, inputStore as G, getHeadlessFontPath as H, drawParagraphNode as I, loadOBJ as J, loadFBX as K, drawSignalNode as L, drawChartNode as M, drawGifNode as N, clearAllVideoCache as O, drawImageNode as P, parseFBX as Q, drawSvgNode as R, SlugFontCache as S, WebGPUAudioProcessor as T, getRenderer2D as U, generateExtrudedTextGeometry as V, initHeadlessWebGPU as W, mediaDecoderCache as X, lutStore as Y, parseColor as Z, Renderer2D as _, Camera3D as a, useRenderContext as at, ShadowPipeline as b, GlassPipeline as c, getEnv as ct, HeadlessGPUCanvasContextMock as d, parseOBJ as et, Light3D as f, RenderProvider as g, Path3D as h, AudioSignalComputePipeline as i, textureCache as it, drawCaptionNode as j, clearVideoNodeState as k, HeadlessChartCanvasElementMock as l, resolveMediaSourceUrl as lt, NodeSurfaceProvider as m, AudioLatentTrackCache as n, shaderStore as nt, ChartGPUEngineBridge as o, GetFontAssetUrl as ot, MotionBlurPipeline as p, loadModel3D as q, AudioMeshDeformPipeline as r, signalRegistry as rt, EffectPipeline as s, R2_CUSTOM_DOMAIN as st, AudioLatentEngine as t, registerHeadlessFont as tt, HeadlessChartContainerMock as u, Renderer3D as v, SlugPipeline as w, SignalRegistry as x, SSAOPipeline as y, drawVideoNode as z };
27514
+ //# sourceMappingURL=dist-8vbhZYqX.mjs.map