@plasius/gpu-shared 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -295,6 +295,33 @@ function getTypeSize(type) {
295
295
  throw new Error(`Unsupported glTF accessor type: ${type}`);
296
296
  }
297
297
  }
298
+ function getComponentByteSize(componentType) {
299
+ switch (componentType) {
300
+ case 5121:
301
+ return 1;
302
+ case 5123:
303
+ return 2;
304
+ case 5125:
305
+ case 5126:
306
+ return 4;
307
+ default:
308
+ throw new Error(`Unsupported glTF componentType: ${componentType}`);
309
+ }
310
+ }
311
+ function readComponentValue(view, componentType, byteOffset) {
312
+ switch (componentType) {
313
+ case 5121:
314
+ return view.getUint8(byteOffset);
315
+ case 5123:
316
+ return view.getUint16(byteOffset, true);
317
+ case 5125:
318
+ return view.getUint32(byteOffset, true);
319
+ case 5126:
320
+ return view.getFloat32(byteOffset, true);
321
+ default:
322
+ throw new Error(`Unsupported glTF componentType: ${componentType}`);
323
+ }
324
+ }
298
325
  function readAccessor(document2, accessorIndex, buffers) {
299
326
  const accessor = document2.accessors?.[accessorIndex];
300
327
  if (!accessor) {
@@ -307,20 +334,255 @@ function readAccessor(document2, accessorIndex, buffers) {
307
334
  const buffer = buffers[bufferView.buffer];
308
335
  const componentCount = getTypeSize(accessor.type);
309
336
  const byteOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
310
- const valueCount = accessor.count * componentCount;
311
- const values = Array.from(
312
- getComponentArray(accessor.componentType, buffer, byteOffset, valueCount)
313
- );
337
+ const componentByteSize = getComponentByteSize(accessor.componentType);
338
+ const packedElementByteLength = componentCount * componentByteSize;
339
+ const byteStride = Math.max(bufferView.byteStride ?? packedElementByteLength, packedElementByteLength);
340
+ let values;
341
+ if (byteStride === packedElementByteLength) {
342
+ const valueCount = accessor.count * componentCount;
343
+ values = Array.from(
344
+ getComponentArray(accessor.componentType, buffer, byteOffset, valueCount)
345
+ );
346
+ } else {
347
+ const view = new DataView(buffer, byteOffset);
348
+ values = new Array(accessor.count * componentCount);
349
+ for (let index = 0; index < accessor.count; index += 1) {
350
+ const elementOffset = index * byteStride;
351
+ for (let componentIndex = 0; componentIndex < componentCount; componentIndex += 1) {
352
+ values[index * componentCount + componentIndex] = readComponentValue(
353
+ view,
354
+ accessor.componentType,
355
+ elementOffset + componentIndex * componentByteSize
356
+ );
357
+ }
358
+ }
359
+ }
314
360
  if (accessor.normalized) {
315
361
  const scale = getNormalizationScale(accessor.componentType);
316
362
  return values.map((value) => value / scale);
317
363
  }
318
364
  return values;
319
365
  }
320
- function getMaterialInfo(document2, primitive) {
366
+ async function decodeImagePixels(blob, urlLabel = "glTF texture") {
367
+ if (typeof createImageBitmap === "function") {
368
+ const bitmap = await createImageBitmap(blob);
369
+ try {
370
+ const canvas = typeof OffscreenCanvas === "function" ? new OffscreenCanvas(bitmap.width, bitmap.height) : typeof document !== "undefined" ? Object.assign(document.createElement("canvas"), {
371
+ width: bitmap.width,
372
+ height: bitmap.height
373
+ }) : null;
374
+ const context = canvas?.getContext?.("2d", { willReadFrequently: true });
375
+ if (!context) {
376
+ throw new Error("Unable to create 2D context for glTF texture decode.");
377
+ }
378
+ context.drawImage(bitmap, 0, 0);
379
+ const imageData = context.getImageData(0, 0, bitmap.width, bitmap.height);
380
+ return Object.freeze({
381
+ width: bitmap.width,
382
+ height: bitmap.height,
383
+ data: imageData.data
384
+ });
385
+ } finally {
386
+ bitmap.close?.();
387
+ }
388
+ }
389
+ if (typeof document === "undefined") {
390
+ throw new Error(`Unable to decode ${urlLabel}: browser image decode APIs are unavailable.`);
391
+ }
392
+ const objectUrl = URL.createObjectURL(blob);
393
+ try {
394
+ const image = await new Promise((resolve, reject) => {
395
+ const element = new Image();
396
+ element.onload = () => resolve(element);
397
+ element.onerror = () => reject(new Error(`Failed to decode ${urlLabel}.`));
398
+ element.src = objectUrl;
399
+ });
400
+ const canvas = Object.assign(document.createElement("canvas"), {
401
+ width: image.naturalWidth || image.width,
402
+ height: image.naturalHeight || image.height
403
+ });
404
+ const context = canvas.getContext("2d", { willReadFrequently: true });
405
+ if (!context) {
406
+ throw new Error("Unable to create 2D context for glTF texture decode.");
407
+ }
408
+ context.drawImage(image, 0, 0);
409
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
410
+ return Object.freeze({
411
+ width: canvas.width,
412
+ height: canvas.height,
413
+ data: imageData.data
414
+ });
415
+ } finally {
416
+ URL.revokeObjectURL(objectUrl);
417
+ }
418
+ }
419
+ function sliceBufferView(document2, bufferViewIndex, buffers) {
420
+ const bufferView = document2.bufferViews?.[bufferViewIndex];
421
+ if (!bufferView) {
422
+ throw new Error(`glTF bufferView ${bufferViewIndex} is missing.`);
423
+ }
424
+ const buffer = buffers[bufferView.buffer];
425
+ if (!buffer) {
426
+ throw new Error(`glTF buffer ${bufferView.buffer} is missing.`);
427
+ }
428
+ const start = bufferView.byteOffset ?? 0;
429
+ const end = start + (bufferView.byteLength ?? 0);
430
+ return buffer.slice(start, end);
431
+ }
432
+ async function loadImageResource(document2, image, index, buffers, baseUrl2) {
433
+ if (typeof image?.uri === "string") {
434
+ const response = await fetch(new URL(image.uri, baseUrl2));
435
+ if (!response.ok) {
436
+ throw new Error(`Failed to load glTF texture: ${response.status} ${response.statusText}`);
437
+ }
438
+ return decodeImagePixels(
439
+ await response.blob(),
440
+ `glTF texture ${index}${image.uri ? ` (${image.uri})` : ""}`
441
+ );
442
+ }
443
+ if (typeof image?.bufferView === "number") {
444
+ const bytes = sliceBufferView(document2, image.bufferView, buffers);
445
+ return decodeImagePixels(
446
+ new Blob([bytes], { type: image.mimeType ?? "application/octet-stream" }),
447
+ `glTF texture ${index}`
448
+ );
449
+ }
450
+ return null;
451
+ }
452
+ function normalizeTextureTransformPair(value, fallback) {
453
+ if (!Array.isArray(value) || value.length < 2) {
454
+ return fallback;
455
+ }
456
+ return [
457
+ Number.isFinite(value[0]) ? Number(value[0]) : fallback[0],
458
+ Number.isFinite(value[1]) ? Number(value[1]) : fallback[1]
459
+ ];
460
+ }
461
+ function readTextureTransform(textureRef) {
462
+ const transformExtension = textureRef?.extensions?.KHR_texture_transform ?? null;
463
+ return {
464
+ texCoord: typeof transformExtension?.texCoord === "number" ? transformExtension.texCoord : textureRef?.texCoord ?? 0,
465
+ offset: normalizeTextureTransformPair(transformExtension?.offset, [0, 0]),
466
+ scale: normalizeTextureTransformPair(transformExtension?.scale, [1, 1]),
467
+ rotation: Number.isFinite(transformExtension?.rotation) ? Number(transformExtension.rotation) : 0
468
+ };
469
+ }
470
+ function wrapTextureCoordinate(value) {
471
+ return (value % 1 + 1) % 1;
472
+ }
473
+ function transformTextureCoordinate(uv, transform) {
474
+ const scaledU = uv[0] * transform.scale[0];
475
+ const scaledV = uv[1] * transform.scale[1];
476
+ const cosine = Math.cos(transform.rotation);
477
+ const sine = Math.sin(transform.rotation);
478
+ return [
479
+ scaledU * cosine - scaledV * sine + transform.offset[0],
480
+ scaledU * sine + scaledV * cosine + transform.offset[1]
481
+ ];
482
+ }
483
+ function readTexturePixel(data, width, height, x, y) {
484
+ const clampedX = Math.min(width - 1, Math.max(0, x));
485
+ const clampedY = Math.min(height - 1, Math.max(0, y));
486
+ const offset = (clampedY * width + clampedX) * 4;
487
+ return [
488
+ data[offset] ?? 0,
489
+ data[offset + 1] ?? 0,
490
+ data[offset + 2] ?? 0,
491
+ data[offset + 3] ?? 255
492
+ ];
493
+ }
494
+ function mixChannel(a, b, weight) {
495
+ return a + (b - a) * weight;
496
+ }
497
+ function sampleTexturePixel(data, width, height, uv) {
498
+ const u = wrapTextureCoordinate(uv[0]);
499
+ const v = wrapTextureCoordinate(uv[1]);
500
+ const sourceX = u * Math.max(width - 1, 0);
501
+ const sourceY = (1 - v) * Math.max(height - 1, 0);
502
+ const x0 = Math.floor(sourceX);
503
+ const y0 = Math.floor(sourceY);
504
+ const x1 = Math.min(width - 1, x0 + 1);
505
+ const y1 = Math.min(height - 1, y0 + 1);
506
+ const tx = sourceX - x0;
507
+ const ty = sourceY - y0;
508
+ const topLeft = readTexturePixel(data, width, height, x0, y0);
509
+ const topRight = readTexturePixel(data, width, height, x1, y0);
510
+ const bottomLeft = readTexturePixel(data, width, height, x0, y1);
511
+ const bottomRight = readTexturePixel(data, width, height, x1, y1);
512
+ return [0, 1, 2, 3].map((channelIndex) => {
513
+ const top = mixChannel(topLeft[channelIndex], topRight[channelIndex], tx);
514
+ const bottom = mixChannel(bottomLeft[channelIndex], bottomRight[channelIndex], tx);
515
+ return mixChannel(top, bottom, ty);
516
+ });
517
+ }
518
+ function applyTextureTransformToPixels(pixels, transform) {
519
+ const isIdentityTransform = transform.offset[0] === 0 && transform.offset[1] === 0 && transform.scale[0] === 1 && transform.scale[1] === 1 && transform.rotation === 0;
520
+ if (isIdentityTransform) {
521
+ return pixels;
522
+ }
523
+ const transformedData = new Uint8ClampedArray(pixels.data.length);
524
+ for (let y = 0; y < pixels.height; y += 1) {
525
+ const outputV = pixels.height > 1 ? 1 - y / (pixels.height - 1) : 0;
526
+ for (let x = 0; x < pixels.width; x += 1) {
527
+ const outputU = pixels.width > 1 ? x / (pixels.width - 1) : 0;
528
+ const sourcePixel = sampleTexturePixel(
529
+ pixels.data,
530
+ pixels.width,
531
+ pixels.height,
532
+ transformTextureCoordinate([outputU, outputV], transform)
533
+ );
534
+ const offset = (y * pixels.width + x) * 4;
535
+ transformedData[offset] = sourcePixel[0];
536
+ transformedData[offset + 1] = sourcePixel[1];
537
+ transformedData[offset + 2] = sourcePixel[2];
538
+ transformedData[offset + 3] = sourcePixel[3];
539
+ }
540
+ }
541
+ return Object.freeze({
542
+ width: pixels.width,
543
+ height: pixels.height,
544
+ data: transformedData
545
+ });
546
+ }
547
+ function getMaterialTexture(document2, textureRef, imageResources) {
548
+ if (!textureRef || typeof textureRef.index !== "number") {
549
+ return null;
550
+ }
551
+ const texture = document2.textures?.[textureRef.index] ?? null;
552
+ const sourceIndex = texture?.source;
553
+ if (typeof sourceIndex !== "number") {
554
+ return null;
555
+ }
556
+ const pixels = imageResources.get(sourceIndex) ?? null;
557
+ if (!pixels) {
558
+ return null;
559
+ }
560
+ const transform = readTextureTransform(textureRef);
561
+ const transformedPixels = applyTextureTransformToPixels(pixels, transform);
562
+ return Object.freeze({
563
+ texCoord: transform.texCoord,
564
+ scale: textureRef.scale,
565
+ strength: textureRef.strength,
566
+ width: transformedPixels.width,
567
+ height: transformedPixels.height,
568
+ data: transformedPixels.data
569
+ });
570
+ }
571
+ function getMaterialInfo(document2, primitive, imageResources) {
321
572
  const material = document2.materials?.[primitive.material] ?? null;
322
- const factor = material?.pbrMetallicRoughness?.baseColorFactor ?? [0.56, 0.33, 0.22, 1];
323
- const emissive = material?.emissiveFactor ?? [0, 0, 0];
573
+ const pbr = material?.pbrMetallicRoughness ?? null;
574
+ const factor = pbr?.baseColorFactor ?? [0.56, 0.33, 0.22, 1];
575
+ const emissive = Array.isArray(material?.emissiveFactor) ? material.emissiveFactor : [0, 0, 0];
576
+ const extensions = material?.extensions ?? {};
577
+ const specular = extensions.KHR_materials_specular ?? null;
578
+ const transmission = extensions.KHR_materials_transmission ?? null;
579
+ const ior = extensions.KHR_materials_ior ?? null;
580
+ const clearcoat = extensions.KHR_materials_clearcoat ?? null;
581
+ const sheen = extensions.KHR_materials_sheen ?? null;
582
+ const volume = extensions.KHR_materials_volume ?? null;
583
+ const iridescence = extensions.KHR_materials_iridescence ?? null;
584
+ const anisotropy = extensions.KHR_materials_anisotropy ?? null;
585
+ const dispersion = extensions.KHR_materials_dispersion ?? null;
324
586
  return Object.freeze({
325
587
  name: material?.name ?? "default-material",
326
588
  color: Object.freeze({
@@ -329,13 +591,88 @@ function getMaterialInfo(document2, primitive) {
329
591
  b: factor[2],
330
592
  a: factor[3] ?? 1
331
593
  }),
332
- roughness: typeof material?.pbrMetallicRoughness?.roughnessFactor === "number" ? material.pbrMetallicRoughness.roughnessFactor : 0.92,
333
- metallic: typeof material?.pbrMetallicRoughness?.metallicFactor === "number" ? material.pbrMetallicRoughness.metallicFactor : 0.08,
594
+ roughness: typeof pbr?.roughnessFactor === "number" ? pbr.roughnessFactor : 0.92,
595
+ metallic: typeof pbr?.metallicFactor === "number" ? pbr.metallicFactor : 0.08,
596
+ opacity: factor[3] ?? 1,
334
597
  emissive: Object.freeze({
335
598
  r: emissive[0] ?? 0,
336
599
  g: emissive[1] ?? 0,
337
- b: emissive[2] ?? 0
338
- })
600
+ b: emissive[2] ?? 0,
601
+ a: 1
602
+ }),
603
+ baseColorTexture: getMaterialTexture(document2, pbr?.baseColorTexture, imageResources),
604
+ metallicRoughnessTexture: getMaterialTexture(
605
+ document2,
606
+ pbr?.metallicRoughnessTexture,
607
+ imageResources
608
+ ),
609
+ normalTexture: getMaterialTexture(document2, material?.normalTexture, imageResources),
610
+ occlusionTexture: getMaterialTexture(document2, material?.occlusionTexture, imageResources),
611
+ emissiveTexture: getMaterialTexture(document2, material?.emissiveTexture, imageResources),
612
+ specular: typeof specular?.specularFactor === "number" ? specular.specularFactor : 1,
613
+ specularColor: Object.freeze(
614
+ Array.isArray(specular?.specularColorFactor) ? [...specular.specularColorFactor] : [1, 1, 1]
615
+ ),
616
+ specularTexture: getMaterialTexture(document2, specular?.specularTexture, imageResources),
617
+ specularColorTexture: getMaterialTexture(
618
+ document2,
619
+ specular?.specularColorTexture,
620
+ imageResources
621
+ ),
622
+ transmission: typeof transmission?.transmissionFactor === "number" ? transmission.transmissionFactor : 0,
623
+ transmissionTexture: getMaterialTexture(
624
+ document2,
625
+ transmission?.transmissionTexture,
626
+ imageResources
627
+ ),
628
+ ior: typeof ior?.ior === "number" ? ior.ior : 1.45,
629
+ attenuationDistance: typeof volume?.attenuationDistance === "number" ? volume.attenuationDistance : null,
630
+ attenuationColor: Object.freeze(
631
+ Array.isArray(volume?.attenuationColor) ? [...volume.attenuationColor] : [1, 1, 1]
632
+ ),
633
+ thickness: typeof volume?.thicknessFactor === "number" ? volume.thicknessFactor : 0,
634
+ thicknessTexture: getMaterialTexture(document2, volume?.thicknessTexture, imageResources),
635
+ clearcoat: typeof clearcoat?.clearcoatFactor === "number" ? clearcoat.clearcoatFactor : 0,
636
+ clearcoatTexture: getMaterialTexture(document2, clearcoat?.clearcoatTexture, imageResources),
637
+ clearcoatRoughness: typeof clearcoat?.clearcoatRoughnessFactor === "number" ? clearcoat.clearcoatRoughnessFactor : 0.08,
638
+ clearcoatRoughnessTexture: getMaterialTexture(
639
+ document2,
640
+ clearcoat?.clearcoatRoughnessTexture,
641
+ imageResources
642
+ ),
643
+ clearcoatNormalTexture: getMaterialTexture(
644
+ document2,
645
+ clearcoat?.clearcoatNormalTexture,
646
+ imageResources
647
+ ),
648
+ sheenColor: Object.freeze(
649
+ Array.isArray(sheen?.sheenColorFactor) ? [...sheen.sheenColorFactor] : [0, 0, 0]
650
+ ),
651
+ sheenColorTexture: getMaterialTexture(document2, sheen?.sheenColorTexture, imageResources),
652
+ sheenRoughness: typeof sheen?.sheenRoughnessFactor === "number" ? sheen.sheenRoughnessFactor : 0,
653
+ sheenRoughnessTexture: getMaterialTexture(
654
+ document2,
655
+ sheen?.sheenRoughnessTexture,
656
+ imageResources
657
+ ),
658
+ iridescence: typeof iridescence?.iridescenceFactor === "number" ? iridescence.iridescenceFactor : 0,
659
+ iridescenceTexture: getMaterialTexture(
660
+ document2,
661
+ iridescence?.iridescenceTexture,
662
+ imageResources
663
+ ),
664
+ iridescenceIor: typeof iridescence?.iridescenceIor === "number" ? iridescence.iridescenceIor : 1.3,
665
+ iridescenceThicknessMinimum: typeof iridescence?.iridescenceThicknessMinimum === "number" ? iridescence.iridescenceThicknessMinimum : 100,
666
+ iridescenceThicknessMaximum: typeof iridescence?.iridescenceThicknessMaximum === "number" ? iridescence.iridescenceThicknessMaximum : 400,
667
+ iridescenceThicknessTexture: getMaterialTexture(
668
+ document2,
669
+ iridescence?.iridescenceThicknessTexture,
670
+ imageResources
671
+ ),
672
+ anisotropy: typeof anisotropy?.anisotropyStrength === "number" ? anisotropy.anisotropyStrength : 0,
673
+ anisotropyRotation: typeof anisotropy?.anisotropyRotation === "number" ? anisotropy.anisotropyRotation : 0,
674
+ anisotropyTexture: getMaterialTexture(document2, anisotropy?.anisotropyTexture, imageResources),
675
+ dispersion: typeof dispersion?.dispersion === "number" ? dispersion.dispersion : 0
339
676
  });
340
677
  }
341
678
  function computeBounds(positions) {
@@ -467,7 +804,7 @@ function transformNormal(normal, matrix) {
467
804
  const length = Math.hypot(transformed[0], transformed[1], transformed[2]) || 1;
468
805
  return [transformed[0] / length, transformed[1] / length, transformed[2] / length];
469
806
  }
470
- function collectScenePrimitives(document2, buffers) {
807
+ function collectScenePrimitives(document2, buffers, imageResources) {
471
808
  const scene = document2.scenes?.[document2.scene ?? 0];
472
809
  if (!scene || !Array.isArray(scene.nodes) || scene.nodes.length === 0) {
473
810
  throw new Error("glTF demo asset must expose a default scene with at least one node.");
@@ -497,6 +834,7 @@ function collectScenePrimitives(document2, buffers) {
497
834
  const positions = readAccessor(document2, primitive.attributes.POSITION, buffers);
498
835
  const normals = typeof primitive.attributes.NORMAL === "number" ? readAccessor(document2, primitive.attributes.NORMAL, buffers) : null;
499
836
  const colors = typeof primitive.attributes.COLOR_0 === "number" ? readAccessor(document2, primitive.attributes.COLOR_0, buffers) : null;
837
+ const uvs = typeof primitive.attributes.TEXCOORD_0 === "number" ? readAccessor(document2, primitive.attributes.TEXCOORD_0, buffers) : null;
500
838
  const transformedPositions = [];
501
839
  const transformedNormals = [];
502
840
  for (let index = 0; index < positions.length; index += 3) {
@@ -514,7 +852,7 @@ function collectScenePrimitives(document2, buffers) {
514
852
  }
515
853
  }
516
854
  const indices = typeof primitive.indices === "number" ? readAccessor(document2, primitive.indices, buffers).map((value) => Number(value)) : Array.from({ length: transformedPositions.length / 3 }, (_, index) => index);
517
- const material = getMaterialInfo(document2, primitive);
855
+ const material = getMaterialInfo(document2, primitive, imageResources);
518
856
  const primitiveName = `${node.name ?? mesh.name ?? "mesh"}-${primitiveIndex}`;
519
857
  results.push(
520
858
  Object.freeze({
@@ -522,6 +860,7 @@ function collectScenePrimitives(document2, buffers) {
522
860
  positions: Object.freeze(transformedPositions),
523
861
  indices: Object.freeze(indices),
524
862
  normals: transformedNormals.length > 0 ? Object.freeze(transformedNormals) : null,
863
+ uvs: uvs ? Object.freeze(uvs) : null,
525
864
  colors: colors ? Object.freeze(colors) : null,
526
865
  material,
527
866
  bounds: computeBounds(transformedPositions)
@@ -577,7 +916,16 @@ async function buildGltfModel(document2, baseUrl2) {
577
916
  return nested.arrayBuffer();
578
917
  })
579
918
  );
580
- const scene = collectScenePrimitives(document2, buffers);
919
+ const imageResources = /* @__PURE__ */ new Map();
920
+ await Promise.all(
921
+ (document2.images ?? []).map(async (image, index) => {
922
+ const pixels = await loadImageResource(document2, image, index, buffers, baseUrl2);
923
+ if (pixels) {
924
+ imageResources.set(index, pixels);
925
+ }
926
+ })
927
+ );
928
+ const scene = collectScenePrimitives(document2, buffers, imageResources);
581
929
  const aggregatePositions = [];
582
930
  const aggregateIndices = [];
583
931
  for (const primitive of scene.primitives) {
@@ -2062,9 +2410,6 @@ __export(product_studio_runtime_exports, {
2062
2410
  createProductStudioMeshes: () => createProductStudioMeshes,
2063
2411
  mountGpuProductStudio: () => mountGpuProductStudio
2064
2412
  });
2065
- function clamp(value, min, max) {
2066
- return Math.max(min, Math.min(max, value));
2067
- }
2068
2413
  function isFiniteVector(value) {
2069
2414
  return Array.isArray(value) && value.length >= 3 && Number.isFinite(value[0]) && Number.isFinite(value[1]) && Number.isFinite(value[2]);
2070
2415
  }
@@ -2253,18 +2598,56 @@ function createProductStudioMeshFromPrimitive(primitive, primitiveIndex, transfo
2253
2598
  const indices = Array.isArray(primitive.indices) && primitive.indices.length >= 3 ? [...primitive.indices] : Array.from({ length: positions.length / 3 }, (_, index) => index);
2254
2599
  const material = primitive.material ?? {};
2255
2600
  const color = readMaterialColor(material);
2601
+ const uvs = Array.isArray(primitive.uvs) ? [...primitive.uvs] : null;
2256
2602
  return Object.freeze({
2257
2603
  id: 1e3 + primitiveIndex,
2258
2604
  positions: Object.freeze(positions),
2259
2605
  indices: Object.freeze(indices),
2260
2606
  normals: Array.isArray(primitive.normals) ? Object.freeze([...primitive.normals]) : null,
2607
+ uvs: uvs ? Object.freeze(uvs) : null,
2608
+ material: Object.freeze({ ...material }),
2261
2609
  color: Object.freeze(color),
2262
2610
  emission: Object.freeze(readEmission(material)),
2263
2611
  materialKind: readMaterialKind(material),
2264
2612
  materialRefId: 1e3 + primitiveIndex,
2265
2613
  roughness: Number.isFinite(material.roughness) ? material.roughness : 0.72,
2266
2614
  metallic: Number.isFinite(material.metallic) ? material.metallic : 0,
2267
- opacity: color[3]
2615
+ opacity: color[3],
2616
+ baseColorTexture: material.baseColorTexture ?? null,
2617
+ metallicRoughnessTexture: material.metallicRoughnessTexture ?? null,
2618
+ normalTexture: material.normalTexture ?? null,
2619
+ occlusionTexture: material.occlusionTexture ?? null,
2620
+ emissiveTexture: material.emissiveTexture ?? null,
2621
+ specular: Number.isFinite(material.specular) ? material.specular : 1,
2622
+ specularColor: Array.isArray(material.specularColor) ? Object.freeze([...material.specularColor]) : void 0,
2623
+ specularTexture: material.specularTexture ?? null,
2624
+ specularColorTexture: material.specularColorTexture ?? null,
2625
+ transmission: Number.isFinite(material.transmission) ? material.transmission : 0,
2626
+ transmissionTexture: material.transmissionTexture ?? null,
2627
+ ior: Number.isFinite(material.ior) ? material.ior : void 0,
2628
+ thickness: Number.isFinite(material.thickness) ? material.thickness : void 0,
2629
+ thicknessTexture: material.thicknessTexture ?? null,
2630
+ attenuationDistance: Number.isFinite(material.attenuationDistance) ? material.attenuationDistance : null,
2631
+ attenuationColor: Array.isArray(material.attenuationColor) ? Object.freeze([...material.attenuationColor]) : void 0,
2632
+ clearcoat: Number.isFinite(material.clearcoat) ? material.clearcoat : void 0,
2633
+ clearcoatTexture: material.clearcoatTexture ?? null,
2634
+ clearcoatRoughness: Number.isFinite(material.clearcoatRoughness) ? material.clearcoatRoughness : void 0,
2635
+ clearcoatRoughnessTexture: material.clearcoatRoughnessTexture ?? null,
2636
+ clearcoatNormalTexture: material.clearcoatNormalTexture ?? null,
2637
+ sheenColor: Array.isArray(material.sheenColor) ? Object.freeze([...material.sheenColor]) : void 0,
2638
+ sheenColorTexture: material.sheenColorTexture ?? null,
2639
+ sheenRoughness: Number.isFinite(material.sheenRoughness) ? material.sheenRoughness : void 0,
2640
+ sheenRoughnessTexture: material.sheenRoughnessTexture ?? null,
2641
+ iridescence: Number.isFinite(material.iridescence) ? material.iridescence : void 0,
2642
+ iridescenceTexture: material.iridescenceTexture ?? null,
2643
+ iridescenceIor: Number.isFinite(material.iridescenceIor) ? material.iridescenceIor : void 0,
2644
+ iridescenceThicknessMinimum: Number.isFinite(material.iridescenceThicknessMinimum) ? material.iridescenceThicknessMinimum : void 0,
2645
+ iridescenceThicknessMaximum: Number.isFinite(material.iridescenceThicknessMaximum) ? material.iridescenceThicknessMaximum : void 0,
2646
+ iridescenceThicknessTexture: material.iridescenceThicknessTexture ?? null,
2647
+ anisotropy: Number.isFinite(material.anisotropy) ? material.anisotropy : void 0,
2648
+ anisotropyRotation: Number.isFinite(material.anisotropyRotation) ? material.anisotropyRotation : void 0,
2649
+ anisotropyTexture: material.anisotropyTexture ?? null,
2650
+ dispersion: Number.isFinite(material.dispersion) ? material.dispersion : void 0
2268
2651
  });
2269
2652
  }
2270
2653
  function createProductStudioMeshes(model, options = {}) {
@@ -2322,16 +2705,10 @@ function resolveRoot(options) {
2322
2705
  }
2323
2706
  return root;
2324
2707
  }
2325
- function resolveRenderSize(root, options) {
2326
- const rect = root.getBoundingClientRect?.() ?? { width: 1280, height: 720 };
2327
- const devicePixelRatio = Number.isFinite(options.devicePixelRatio) ? options.devicePixelRatio : Number.isFinite(globalThis.window?.devicePixelRatio) ? globalThis.window.devicePixelRatio : 1;
2328
- const cssWidth = Number.isFinite(rect.width) && rect.width > 0 ? rect.width : 1280;
2329
- const cssHeight = Number.isFinite(rect.height) && rect.height > 0 ? rect.height : cssWidth * (9 / 16);
2330
- const width = Number.isFinite(options.width) ? Math.trunc(options.width) : clamp(Math.round(cssWidth * devicePixelRatio), 640, 1920);
2331
- const height = Number.isFinite(options.height) ? Math.trunc(options.height) : clamp(Math.round(cssHeight * devicePixelRatio), 360, 1080);
2708
+ function resolveRenderSize(options) {
2332
2709
  return {
2333
- width,
2334
- height
2710
+ width: Number.isFinite(options.width) ? Math.trunc(options.width) : DEFAULT_RENDER_WIDTH,
2711
+ height: Number.isFinite(options.height) ? Math.trunc(options.height) : DEFAULT_RENDER_HEIGHT
2335
2712
  };
2336
2713
  }
2337
2714
  function installSnapshotHook(state) {
@@ -2402,13 +2779,13 @@ async function mountGpuProductStudio(options = {}, featureFlags = null) {
2402
2779
  if (typeof rendererModule.createWavefrontPathTracingComputeRenderer !== "function") {
2403
2780
  throw new Error("Product Studio renderer loader must provide createWavefrontPathTracingComputeRenderer.");
2404
2781
  }
2405
- const size = resolveRenderSize(root, options);
2782
+ const size = resolveRenderSize(options);
2406
2783
  const lightingOptions = await resolveWavefrontLightingOptions(options);
2407
2784
  const renderer = await rendererModule.createWavefrontPathTracingComputeRenderer({
2408
2785
  canvas,
2409
2786
  width: size.width,
2410
2787
  height: size.height,
2411
- maxDepth: Number.isFinite(options.maxDepth) ? options.maxDepth : 6,
2788
+ maxDepth: Number.isFinite(options.maxDepth) ? options.maxDepth : DEFAULT_RENDER_MAX_DEPTH,
2412
2789
  tileSize: Number.isFinite(options.tileSize) ? options.tileSize : 128,
2413
2790
  samplesPerPixel: Number.isFinite(options.samplesPerPixel) ? options.samplesPerPixel : 8,
2414
2791
  denoise: options.denoise !== false,
@@ -2450,7 +2827,7 @@ async function mountGpuProductStudio(options = {}, featureFlags = null) {
2450
2827
  }
2451
2828
  });
2452
2829
  }
2453
- var STYLE_ID, DEFAULT_PRODUCT_ASSET_URL, DEFAULT_TARGET_CENTER, DEFAULT_TARGET_SIZE;
2830
+ var STYLE_ID, DEFAULT_PRODUCT_ASSET_URL, DEFAULT_TARGET_CENTER, DEFAULT_TARGET_SIZE, DEFAULT_RENDER_WIDTH, DEFAULT_RENDER_HEIGHT, DEFAULT_RENDER_MAX_DEPTH;
2454
2831
  var init_product_studio_runtime = __esm({
2455
2832
  "src/product-studio-runtime.js"() {
2456
2833
  init_gltf_loader();
@@ -2458,6 +2835,9 @@ var init_product_studio_runtime = __esm({
2458
2835
  DEFAULT_PRODUCT_ASSET_URL = "/data/models/eames-lounge-chair-ottoman/Eames_Lounge_Chair_Ottoman.gltf";
2459
2836
  DEFAULT_TARGET_CENTER = Object.freeze([0, 0.74, 0]);
2460
2837
  DEFAULT_TARGET_SIZE = 2.25;
2838
+ DEFAULT_RENDER_WIDTH = 640;
2839
+ DEFAULT_RENDER_HEIGHT = 360;
2840
+ DEFAULT_RENDER_MAX_DEPTH = 2;
2461
2841
  }
2462
2842
  });
2463
2843
 
@@ -2578,17 +2958,17 @@ function createFallbackPerformanceFeatureModule() {
2578
2958
  let pressureLevel = "stable";
2579
2959
  let frameSamples = 0;
2580
2960
  let averageMs = 16.67;
2581
- const clamp3 = (next = 16.67) => Number.isFinite(next) ? Math.max(1, next) : 16.67;
2961
+ const clamp2 = (next = 16.67) => Number.isFinite(next) ? Math.max(1, next) : 16.67;
2582
2962
  const target = Object.freeze({
2583
2963
  targetFrameTimeMs: 16.67,
2584
- downgradeFrameTimeMs: clamp3(adaptation?.degradeThresholdMs ?? 20),
2585
- upgradeFrameTimeMs: clamp3(adaptation?.upgradeThresholdMs ?? 14)
2964
+ downgradeFrameTimeMs: clamp2(adaptation?.degradeThresholdMs ?? 20),
2965
+ upgradeFrameTimeMs: clamp2(adaptation?.upgradeThresholdMs ?? 14)
2586
2966
  });
2587
2967
  return {
2588
2968
  recordFrame({ frameTimeMs = averageMs } = {}) {
2589
2969
  const sample = Number.isFinite(Number(frameTimeMs)) ? Number(frameTimeMs) : averageMs;
2590
2970
  frameSamples += 1;
2591
- averageMs = clamp3((averageMs * (frameSamples - 1) + sample) / frameSamples);
2971
+ averageMs = clamp2((averageMs * (frameSamples - 1) + sample) / frameSamples);
2592
2972
  const fps = 1e3 / averageMs;
2593
2973
  pressureLevel = sample > target.downgradeFrameTimeMs ? "degrade" : pressureLevel === "degrade" && sample <= target.upgradeFrameTimeMs ? "stable" : pressureLevel;
2594
2974
  return {
@@ -3269,14 +3649,14 @@ function injectStyles() {
3269
3649
  `;
3270
3650
  document.head.appendChild(style);
3271
3651
  }
3272
- function clamp2(value, min, max) {
3652
+ function clamp(value, min, max) {
3273
3653
  return Math.max(min, Math.min(max, value));
3274
3654
  }
3275
3655
  function mix(a, b, t) {
3276
3656
  return a + (b - a) * t;
3277
3657
  }
3278
3658
  function smoothstep(min, max, value) {
3279
- const t = clamp2((value - min) / Math.max(1e-4, max - min), 0, 1);
3659
+ const t = clamp((value - min) / Math.max(1e-4, max - min), 0, 1);
3280
3660
  return t * t * (3 - 2 * t);
3281
3661
  }
3282
3662
  function pseudoRandom(seed) {
@@ -3360,10 +3740,10 @@ function projectPoint(point, camera, viewport) {
3360
3740
  };
3361
3741
  }
3362
3742
  function colorToRgba(color, alpha = 1) {
3363
- const r = Math.round(clamp2(color.r, 0, 1) * 255);
3364
- const g = Math.round(clamp2(color.g, 0, 1) * 255);
3365
- const b = Math.round(clamp2(color.b, 0, 1) * 255);
3366
- return `rgba(${r}, ${g}, ${b}, ${clamp2(alpha, 0, 1)})`;
3743
+ const r = Math.round(clamp(color.r, 0, 1) * 255);
3744
+ const g = Math.round(clamp(color.g, 0, 1) * 255);
3745
+ const b = Math.round(clamp(color.b, 0, 1) * 255);
3746
+ return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1)})`;
3367
3747
  }
3368
3748
  function mixColor(a, b, t) {
3369
3749
  return {
@@ -3437,12 +3817,12 @@ function projectShadowPoint(point, lightDir, planeY) {
3437
3817
  return addVec3(point, scaleVec3(shadowDir, distance));
3438
3818
  }
3439
3819
  function shadeColor(base, normal, lightDir, heightBias = 0, accent = 0) {
3440
- const diffuse = clamp2(dotVec3(normalizeVec3(normal), lightDir), 0, 1);
3820
+ const diffuse = clamp(dotVec3(normalizeVec3(normal), lightDir), 0, 1);
3441
3821
  const brightness = 0.24 + diffuse * 0.72 + heightBias * 0.08 + accent;
3442
3822
  return {
3443
- r: clamp2(base.r * brightness, 0, 1),
3444
- g: clamp2(base.g * brightness, 0, 1),
3445
- b: clamp2(base.b * (brightness + 0.03), 0, 1)
3823
+ r: clamp(base.r * brightness, 0, 1),
3824
+ g: clamp(base.g * brightness, 0, 1),
3825
+ b: clamp(base.b * (brightness + 0.03), 0, 1)
3446
3826
  };
3447
3827
  }
3448
3828
  function getMaterialSeed(materialName) {
@@ -3480,13 +3860,13 @@ function applyMaterialDetail(color, material, worldCenter, normal, surfaceType)
3480
3860
  const sample = worldCenter.x * 3.17 + worldCenter.y * 5.29 + worldCenter.z * 7.83 + getMaterialSeed(materialName) * 0.013;
3481
3861
  const grain = (pseudoRandom(sample) - 0.5) * detailStrength;
3482
3862
  const lowerSurface = smoothstep(7.5, -0.8, worldCenter.y);
3483
- const verticalSurface = 1 - clamp2(Math.abs(normal.y), 0, 1);
3863
+ const verticalSurface = 1 - clamp(Math.abs(normal.y), 0, 1);
3484
3864
  const materialLowerWear = /stone|concrete|plaster|paint|wood|timber|plank|crate/.test(materialName.toLowerCase()) ? lowerSurface * verticalSurface * 0.055 : 0;
3485
3865
  const wetlineWear = surfaceType === "ship" && worldCenter.y < 0.72 ? smoothstep(0.72, -0.1, worldCenter.y) * 0.05 : 0;
3486
3866
  return {
3487
- r: clamp2(color.r * (1 + grain) - materialLowerWear - wetlineWear, 0, 1),
3488
- g: clamp2(color.g * (1 + grain * 0.82) - materialLowerWear * 0.9 - wetlineWear, 0, 1),
3489
- b: clamp2(color.b * (1 + grain * 0.62) - materialLowerWear * 0.68 - wetlineWear * 0.75, 0, 1)
3867
+ r: clamp(color.r * (1 + grain) - materialLowerWear - wetlineWear, 0, 1),
3868
+ g: clamp(color.g * (1 + grain * 0.82) - materialLowerWear * 0.9 - wetlineWear, 0, 1),
3869
+ b: clamp(color.b * (1 + grain * 0.62) - materialLowerWear * 0.68 - wetlineWear * 0.75, 0, 1)
3490
3870
  };
3491
3871
  }
3492
3872
  function buildCamera(state, canvas) {
@@ -3912,7 +4292,7 @@ function resizeCanvasToDisplaySize(canvas, state) {
3912
4292
  const deviceScale = readPositiveNumber(globalThis.devicePixelRatio, 1);
3913
4293
  const requestedScale = readPositiveNumber(state.renderScale, deviceScale);
3914
4294
  const maxScale = state.captureMode ? 2 : 1.5;
3915
- let scale = clamp2(requestedScale, 1, maxScale);
4295
+ let scale = clamp(requestedScale, 1, maxScale);
3916
4296
  const pixelBudget = state.captureMode ? CAPTURE_CANVAS_PIXEL_BUDGET : DEFAULT_CANVAS_WIDTH * DEFAULT_CANVAS_HEIGHT * 1.5;
3917
4297
  const projectedPixels = width * height * scale * scale;
3918
4298
  if (projectedPixels > pixelBudget) {
@@ -4097,12 +4477,12 @@ function satisfyClothConstraint(clothState, constraint) {
4097
4477
  }
4098
4478
  }
4099
4479
  function advanceShowcaseClothSimulationState(clothState, options = {}) {
4100
- const dt = clamp2(options.dt ?? 1 / 60, 1 / 240, 1 / 18);
4480
+ const dt = clamp(options.dt ?? 1 / 60, 1 / 240, 1 / 18);
4101
4481
  const time = readVisualNumber(options.time, 0);
4102
4482
  const flagMotion = readVisualNumber(options.flagMotion, 0.92);
4103
4483
  const waveInfluence = readVisualNumber(options.waveInfluence, 0);
4104
4484
  const wrinkleLayers = Math.max(1, clothState.representation.mesh?.wrinkleLayers ?? 2);
4105
- const solverIterations = clamp2(
4485
+ const solverIterations = clamp(
4106
4486
  Math.round(clothState.representation.mesh?.solverIterations ?? 6),
4107
4487
  2,
4108
4488
  10
@@ -4345,7 +4725,7 @@ function buildWaterMotionEffects(state) {
4345
4725
  impulse.z
4346
4726
  ),
4347
4727
  radius,
4348
- opacity: clamp2(impulse.life * 0.28, 0.08, 0.3)
4728
+ opacity: clamp(impulse.life * 0.28, 0.08, 0.3)
4349
4729
  });
4350
4730
  });
4351
4731
  for (const ship of state.ships) {
@@ -4377,7 +4757,7 @@ function buildWaterMotionEffects(state) {
4377
4757
  }
4378
4758
  wakeTrails.push(
4379
4759
  Object.freeze({
4380
- opacity: clamp2(0.18 + speed * 0.09, 0.22, 0.46),
4760
+ opacity: clamp(0.18 + speed * 0.09, 0.22, 0.46),
4381
4761
  points: Object.freeze(points)
4382
4762
  })
4383
4763
  );
@@ -4591,7 +4971,7 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
4591
4971
  const y = pseudoRandom(index * 7 + 5) * canvas.height * 0.42;
4592
4972
  const twinkle = 0.45 + Math.sin(state.time * 1.4 + index * 0.73) * 0.25;
4593
4973
  const radius = 0.6 + pseudoRandom(index * 11 + 2) * 1.9;
4594
- ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp2(twinkle, 0.16, 0.92)})`);
4974
+ ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp(twinkle, 0.16, 0.92)})`);
4595
4975
  ctx.beginPath();
4596
4976
  ctx.arc(x, y, radius, 0, Math.PI * 2);
4597
4977
  ctx.fill();
@@ -4643,7 +5023,7 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
4643
5023
  if (state.collisionFlash > 0.01) {
4644
5024
  ctx.fillStyle = visuals.collisionFlash.replace(
4645
5025
  /[\d.]+\)$/u,
4646
- `${clamp2(state.collisionFlash * 0.22, 0, 0.26)})`
5026
+ `${clamp(state.collisionFlash * 0.22, 0, 0.26)})`
4647
5027
  );
4648
5028
  ctx.fillRect(0, 0, canvas.width, canvas.height);
4649
5029
  }
@@ -4662,7 +5042,7 @@ function resolveLocalLightContribution(triangle, lightSources) {
4662
5042
  continue;
4663
5043
  }
4664
5044
  const lightDir = normalizeVec3(delta);
4665
- const facing = clamp2(dotVec3(normal, lightDir), 0, 1);
5045
+ const facing = clamp(dotVec3(normal, lightDir), 0, 1);
4666
5046
  const response = attenuation * (0.18 + facing * 0.82);
4667
5047
  const glowColor = source.glowColor ?? source.coreColor ?? { r: 1, g: 0.72, b: 0.4 };
4668
5048
  contribution.r += glowColor.r * response * 0.32;
@@ -4684,31 +5064,31 @@ function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, sha
4684
5064
  triangle.baseColor,
4685
5065
  surfaceNormal,
4686
5066
  lightDir,
4687
- clamp2((triangle.worldCenter.y + 3) / 10, 0, 1),
5067
+ clamp((triangle.worldCenter.y + 3) / 10, 0, 1),
4688
5068
  triangle.accent
4689
5069
  );
4690
5070
  const reflection = reflectionStrength * (triangle.reflection ?? 0);
4691
5071
  const viewDir = normalizeVec3(subVec3(camera.eye, triangle.worldCenter));
4692
5072
  const reflectedLight = reflectVec3(scaleVec3(lightDir, -1), surfaceNormal);
4693
- const gloss = mix(0.78, 0.14, clamp2(material.roughness ?? 0.88, 0, 1)) + (material.metallic ?? 0) * 0.18;
4694
- const specularPower = mix(26, 7, clamp2(material.roughness ?? 0.88, 0, 1));
4695
- const specular = Math.pow(clamp2(dotVec3(reflectedLight, viewDir), 0, 1), specularPower) * gloss;
5073
+ const gloss = mix(0.78, 0.14, clamp(material.roughness ?? 0.88, 0, 1)) + (material.metallic ?? 0) * 0.18;
5074
+ const specularPower = mix(26, 7, clamp(material.roughness ?? 0.88, 0, 1));
5075
+ const specular = Math.pow(clamp(dotVec3(reflectedLight, viewDir), 0, 1), specularPower) * gloss;
4696
5076
  const emissive = material.emissive ?? { r: 0, g: 0, b: 0 };
4697
5077
  const localLight = resolveLocalLightContribution(triangle, localLights);
4698
5078
  const occlusion = triangle.surfaceType === "water" ? shadowStrength * 0.018 : shadowStrength * 0.04;
4699
5079
  const detailed = applyMaterialDetail(
4700
5080
  {
4701
- r: clamp2(
5081
+ r: clamp(
4702
5082
  shaded.r + reflection * 0.08 + specular * 0.16 + emissive.r * 0.42 + localLight.r - occlusion,
4703
5083
  0,
4704
5084
  1
4705
5085
  ),
4706
- g: clamp2(
5086
+ g: clamp(
4707
5087
  shaded.g + reflection * 0.08 + specular * 0.16 + emissive.g * 0.42 + localLight.g - occlusion,
4708
5088
  0,
4709
5089
  1
4710
5090
  ),
4711
- b: clamp2(
5091
+ b: clamp(
4712
5092
  shaded.b + reflection * 0.16 + specular * 0.22 + emissive.b * 0.46 + localLight.b - occlusion * 0.5,
4713
5093
  0,
4714
5094
  1
@@ -4740,7 +5120,7 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
4740
5120
  }
4741
5121
  ctx.save();
4742
5122
  ctx.globalCompositeOperation = "multiply";
4743
- ctx.fillStyle = options.color ?? `rgba(12, 24, 36, ${clamp2(options.alpha ?? 0.16, 0, 0.5)})`;
5123
+ ctx.fillStyle = options.color ?? `rgba(12, 24, 36, ${clamp(options.alpha ?? 0.16, 0, 0.5)})`;
4744
5124
  ctx.shadowColor = options.color ?? "rgba(12, 24, 36, 0.22)";
4745
5125
  ctx.shadowBlur = options.blur ?? 18;
4746
5126
  ctx.beginPath();
@@ -4977,7 +5357,7 @@ function resolveShipRoute(ship, state, radius) {
4977
5357
  const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
4978
5358
  const laneCenter = ship.id === "northwind" ? 10.2 + wander * 2.1 + crossCurrent * 0.6 : 7 + wander * 3.3 - crossCurrent * 1.1;
4979
5359
  const targetX = ship.routeDirection > 0 ? HARBOR_BOUNDS.maxX - radius * 1.7 : HARBOR_BOUNDS.minX + radius * 1.7;
4980
- return vec3(targetX, 0, clamp2(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
5360
+ return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
4981
5361
  }
4982
5362
  function updateShipMotion(state, ship, dt, shipModel) {
4983
5363
  const physics = shipModel.physics;
@@ -5090,7 +5470,7 @@ function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
5090
5470
  a.velocity = subVec3(a.velocity, scaleVec3(impulse, invMassA));
5091
5471
  b.velocity = addVec3(b.velocity, scaleVec3(impulse, invMassB));
5092
5472
  const tangentSpeed = dotVec3(relativeVelocity, tangent);
5093
- const frictionMagnitude = clamp2(
5473
+ const frictionMagnitude = clamp(
5094
5474
  -tangentSpeed / Math.max(1e-4, invMassSum),
5095
5475
  -impulseMagnitude * 0.16,
5096
5476
  impulseMagnitude * 0.16
@@ -5111,14 +5491,14 @@ function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
5111
5491
  state.waveImpulses.push({
5112
5492
  x: contactPoint.x,
5113
5493
  z: contactPoint.z,
5114
- strength: clamp2(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
5494
+ strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
5115
5495
  radius: 0.9 + penetration * 1.4,
5116
5496
  life: 1
5117
5497
  });
5118
5498
  state.collisionCount += 1;
5119
5499
  state.collisionFlash = Math.max(
5120
5500
  state.collisionFlash,
5121
- clamp2(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
5501
+ clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
5122
5502
  );
5123
5503
  a.collisionCooldown = 0.2;
5124
5504
  b.collisionCooldown = 0.2;
@@ -5234,8 +5614,8 @@ function renderSprays(ctx, sprays, camera, viewport) {
5234
5614
  if (!projected) {
5235
5615
  continue;
5236
5616
  }
5237
- const radius = clamp2(1 / projected.depth * 260, 1.5, 7.5);
5238
- ctx.fillStyle = `rgba(225, 243, 250, ${clamp2(spray.life / 1.6, 0, 0.9)})`;
5617
+ const radius = clamp(1 / projected.depth * 260, 1.5, 7.5);
5618
+ ctx.fillStyle = `rgba(225, 243, 250, ${clamp(spray.life / 1.6, 0, 0.9)})`;
5239
5619
  ctx.beginPath();
5240
5620
  ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
5241
5621
  ctx.fill();
@@ -5345,7 +5725,7 @@ function renderDirectLightGlow(ctx, source, camera, viewport) {
5345
5725
  if (!projected) {
5346
5726
  return;
5347
5727
  }
5348
- const radius = clamp2(1 / projected.depth * 420 * source.glowScale, 4, 34);
5728
+ const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
5349
5729
  const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
5350
5730
  halo.addColorStop(0, colorToRgba(source.coreColor, 0.98));
5351
5731
  halo.addColorStop(0.5, colorToRgba(source.glowColor, 0.42));
@@ -5367,7 +5747,7 @@ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
5367
5747
  if (!projected) {
5368
5748
  return;
5369
5749
  }
5370
- const radius = clamp2(1 / projected.depth * 420 * source.glowScale, 4, 34);
5750
+ const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
5371
5751
  const waterline = sampleWave(state, source.point.x, source.point.z, state.time) * 0.22;
5372
5752
  const reflectedPoint = vec3(
5373
5753
  source.point.x,
@@ -5456,14 +5836,14 @@ function renderLighthouseBeam(ctx, state, camera, viewport, visuals) {
5456
5836
  2,
5457
5837
  projectedSource.x,
5458
5838
  projectedSource.y,
5459
- clamp2(beamLength * 0.22, 18, 80)
5839
+ clamp(beamLength * 0.22, 18, 80)
5460
5840
  );
5461
5841
  core.addColorStop(0, colorToRgba(visuals.torchCore, 0.58));
5462
5842
  core.addColorStop(0.5, colorToRgba(visuals.torchGlow, 0.18));
5463
5843
  core.addColorStop(1, colorToRgba(visuals.torchGlow, 0));
5464
5844
  ctx.fillStyle = core;
5465
5845
  ctx.beginPath();
5466
- ctx.arc(projectedSource.x, projectedSource.y, clamp2(beamLength * 0.18, 14, 64), 0, Math.PI * 2);
5846
+ ctx.arc(projectedSource.x, projectedSource.y, clamp(beamLength * 0.18, 14, 64), 0, Math.PI * 2);
5467
5847
  ctx.fill();
5468
5848
  ctx.restore();
5469
5849
  }
@@ -5513,7 +5893,7 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
5513
5893
  }
5514
5894
  const averageDepth = projected.reduce((total, entry) => total + entry.projected.depth, 0) / projected.length;
5515
5895
  const averageWidth = projected.reduce((total, entry) => total + entry.width, 0) / projected.length;
5516
- const baseWidth = clamp2(averageWidth / Math.max(0.25, averageDepth) * 180, 1.6, 5.4);
5896
+ const baseWidth = clamp(averageWidth / Math.max(0.25, averageDepth) * 180, 1.6, 5.4);
5517
5897
  ctx.strokeStyle = `rgba(146, 194, 236, ${wake.opacity * 0.52})`;
5518
5898
  ctx.lineWidth = baseWidth * 1.9;
5519
5899
  ctx.lineCap = "round";
@@ -5559,7 +5939,7 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
5559
5939
  const radiusX = Math.hypot(xAxis.x - center.x, xAxis.y - center.y);
5560
5940
  const radiusY = Math.hypot(zAxis.x - center.x, zAxis.y - center.y);
5561
5941
  ctx.strokeStyle = `rgba(216, 235, 255, ${ring.opacity})`;
5562
- ctx.lineWidth = clamp2((radiusX + radiusY) * 0.02, 1, 3.1);
5942
+ ctx.lineWidth = clamp((radiusX + radiusY) * 0.02, 1, 3.1);
5563
5943
  ctx.beginPath();
5564
5944
  ctx.ellipse(center.x, center.y, radiusX, radiusY, 0, 0, Math.PI * 2);
5565
5945
  ctx.stroke();