@plasius/gpu-shared 0.1.1 → 0.1.3

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.
@@ -4216,6 +4216,12 @@ var gpuResourceCategories = Object.freeze([
4216
4216
  "pipeline",
4217
4217
  "custom"
4218
4218
  ]);
4219
+ var gpuPipelinePhases = Object.freeze([
4220
+ "simulation",
4221
+ "secondary-simulation",
4222
+ "scene-preparation",
4223
+ "render"
4224
+ ]);
4219
4225
  function isRecord2(value) {
4220
4226
  return typeof value === "object" && value !== null && !Array.isArray(value);
4221
4227
  }
@@ -4329,6 +4335,7 @@ var DEFAULT_OPTIONS = Object.freeze({
4329
4335
  maxRetainedQueueSamples: 240,
4330
4336
  maxRetainedReadyLaneSamples: 240,
4331
4337
  maxRetainedDependencyUnlockSamples: 240,
4338
+ maxRetainedPipelinePhaseSamples: 240,
4332
4339
  maxRetainedFrameSamples: 240,
4333
4340
  maxTrackedAllocations: 512
4334
4341
  });
@@ -4336,7 +4343,8 @@ var LIMITATIONS = Object.freeze([
4336
4343
  "Tracked memory reflects only allocations reported to this debug session.",
4337
4344
  "Portable WebGPU does not expose authoritative live GPU core-count or total-memory counters.",
4338
4345
  "Hardware hints are optional caller-supplied metadata and may be platform-specific.",
4339
- "Ready-lane and dependency-unlock diagnostics are caller-reported integration samples, not automatic WebGPU counters."
4346
+ "Ready-lane and dependency-unlock diagnostics are caller-reported integration samples, not automatic WebGPU counters.",
4347
+ "Pipeline phase and snapshot-lag diagnostics are caller-reported integration samples, not automatic WebGPU counters."
4340
4348
  ]);
4341
4349
  function clampCount(value, fallback) {
4342
4350
  if (!value || !Number.isFinite(value) || value <= 0) {
@@ -4497,6 +4505,40 @@ function normalizeDependencyUnlockSample(sample) {
4497
4505
  frameId: sample.frameId === void 0 ? void 0 : assertIdentifier4("dependencyUnlock.frameId", sample.frameId)
4498
4506
  };
4499
4507
  }
4508
+ function normalizePipelinePhaseSample(sample) {
4509
+ if (sample.signal !== void 0 && !isAbortSignalLike2(sample.signal)) {
4510
+ throw new Error("pipelinePhase.signal must be an AbortSignal when provided.");
4511
+ }
4512
+ const snapshotAgeFrames = readNonNegativeNumber2(
4513
+ "pipelinePhase.snapshotAgeFrames",
4514
+ sample.snapshotAgeFrames
4515
+ );
4516
+ if (snapshotAgeFrames !== void 0 && !Number.isInteger(snapshotAgeFrames)) {
4517
+ throw new Error(
4518
+ "pipelinePhase.snapshotAgeFrames must be an integer greater than or equal to zero."
4519
+ );
4520
+ }
4521
+ return {
4522
+ owner: assertIdentifier4("pipelinePhase.owner", sample.owner),
4523
+ pipeline: assertEnumValue4(
4524
+ "pipelinePhase.pipeline",
4525
+ sample.pipeline,
4526
+ gpuPipelinePhases
4527
+ ),
4528
+ stage: assertIdentifier4("pipelinePhase.stage", sample.stage),
4529
+ frameId: sample.frameId === void 0 ? void 0 : assertIdentifier4("pipelinePhase.frameId", sample.frameId),
4530
+ durationMs: readNonNegativeNumber2(
4531
+ "pipelinePhase.durationMs",
4532
+ sample.durationMs
4533
+ ),
4534
+ snapshotFrameId: sample.snapshotFrameId === void 0 ? void 0 : assertIdentifier4("pipelinePhase.snapshotFrameId", sample.snapshotFrameId),
4535
+ snapshotAgeFrames,
4536
+ snapshotAgeMs: readNonNegativeNumber2(
4537
+ "pipelinePhase.snapshotAgeMs",
4538
+ sample.snapshotAgeMs
4539
+ )
4540
+ };
4541
+ }
4500
4542
  function createGpuDebugSession(options = {}) {
4501
4543
  const settings = {
4502
4544
  enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
@@ -4516,6 +4558,10 @@ function createGpuDebugSession(options = {}) {
4516
4558
  options.maxRetainedDependencyUnlockSamples,
4517
4559
  DEFAULT_OPTIONS.maxRetainedDependencyUnlockSamples
4518
4560
  ),
4561
+ maxRetainedPipelinePhaseSamples: clampCount(
4562
+ options.maxRetainedPipelinePhaseSamples,
4563
+ DEFAULT_OPTIONS.maxRetainedPipelinePhaseSamples
4564
+ ),
4519
4565
  maxRetainedFrameSamples: clampCount(
4520
4566
  options.maxRetainedFrameSamples,
4521
4567
  DEFAULT_OPTIONS.maxRetainedFrameSamples
@@ -4533,6 +4579,7 @@ function createGpuDebugSession(options = {}) {
4533
4579
  const readyLaneSamples = [];
4534
4580
  const dispatchSamples = [];
4535
4581
  const dependencyUnlockSamples = [];
4582
+ const pipelinePhaseSamples = [];
4536
4583
  const frameSamples = [];
4537
4584
  let peakTrackedBytes = 0;
4538
4585
  const totalTrackedBytes = () => [...allocations.values()].reduce((total, allocation) => total + allocation.sizeBytes, 0);
@@ -4684,6 +4731,66 @@ function createGpuDebugSession(options = {}) {
4684
4731
  )
4685
4732
  };
4686
4733
  };
4734
+ const buildPipelineSnapshot = () => {
4735
+ const durations = pipelinePhaseSamples.map((sample) => sample.durationMs).filter((value) => value !== void 0);
4736
+ const snapshotAgeMsValues = pipelinePhaseSamples.map((sample) => sample.snapshotAgeMs).filter((value) => value !== void 0);
4737
+ const snapshotAgeFrameValues = pipelinePhaseSamples.map((sample) => sample.snapshotAgeFrames).filter((value) => value !== void 0);
4738
+ const byPipeline = /* @__PURE__ */ new Map();
4739
+ for (const sample of pipelinePhaseSamples) {
4740
+ const bucket = byPipeline.get(sample.pipeline) ?? {
4741
+ pipeline: sample.pipeline,
4742
+ sampleCount: 0,
4743
+ totalDurationMs: 0,
4744
+ durationValues: [],
4745
+ snapshotAgeMsValues: [],
4746
+ snapshotAgeFramesValues: []
4747
+ };
4748
+ bucket.sampleCount += 1;
4749
+ bucket.totalDurationMs += sample.durationMs ?? 0;
4750
+ if (sample.durationMs !== void 0) {
4751
+ bucket.durationValues.push(sample.durationMs);
4752
+ }
4753
+ if (sample.snapshotAgeMs !== void 0) {
4754
+ bucket.snapshotAgeMsValues.push(sample.snapshotAgeMs);
4755
+ }
4756
+ if (sample.snapshotAgeFrames !== void 0) {
4757
+ bucket.snapshotAgeFramesValues.push(sample.snapshotAgeFrames);
4758
+ }
4759
+ byPipeline.set(sample.pipeline, bucket);
4760
+ }
4761
+ const hottestStages = pipelinePhaseSamples.map((sample) => ({
4762
+ owner: sample.owner,
4763
+ pipeline: sample.pipeline,
4764
+ stage: sample.stage,
4765
+ frameId: sample.frameId,
4766
+ durationMs: sample.durationMs,
4767
+ snapshotFrameId: sample.snapshotFrameId,
4768
+ snapshotAgeFrames: sample.snapshotAgeFrames,
4769
+ snapshotAgeMs: sample.snapshotAgeMs
4770
+ })).sort((left, right) => {
4771
+ const leftScore = left.durationMs ?? left.snapshotAgeMs ?? left.snapshotAgeFrames ?? 0;
4772
+ const rightScore = right.durationMs ?? right.snapshotAgeMs ?? right.snapshotAgeFrames ?? 0;
4773
+ return rightScore - leftScore;
4774
+ }).slice(0, 5);
4775
+ return {
4776
+ sampleCount: pipelinePhaseSamples.length,
4777
+ totalDurationMs: durations.reduce((total, value) => total + value, 0),
4778
+ averageDurationMs: average(durations),
4779
+ averageSnapshotAgeMs: average(snapshotAgeMsValues),
4780
+ maxSnapshotAgeMs: snapshotAgeMsValues.length > 0 ? Math.max(...snapshotAgeMsValues) : void 0,
4781
+ maxSnapshotAgeFrames: snapshotAgeFrameValues.length > 0 ? Math.max(...snapshotAgeFrameValues) : void 0,
4782
+ byPipeline: [...byPipeline.values()].map((bucket) => ({
4783
+ pipeline: bucket.pipeline,
4784
+ sampleCount: bucket.sampleCount,
4785
+ totalDurationMs: bucket.totalDurationMs,
4786
+ averageDurationMs: average(bucket.durationValues),
4787
+ averageSnapshotAgeMs: average(bucket.snapshotAgeMsValues),
4788
+ maxSnapshotAgeMs: bucket.snapshotAgeMsValues.length > 0 ? Math.max(...bucket.snapshotAgeMsValues) : void 0,
4789
+ maxSnapshotAgeFrames: bucket.snapshotAgeFramesValues.length > 0 ? Math.max(...bucket.snapshotAgeFramesValues) : void 0
4790
+ })).sort((left, right) => right.totalDurationMs - left.totalDurationMs),
4791
+ hottestStages
4792
+ };
4793
+ };
4687
4794
  return {
4688
4795
  isEnabled() {
4689
4796
  return enabled;
@@ -4754,6 +4861,17 @@ function createGpuDebugSession(options = {}) {
4754
4861
  );
4755
4862
  return true;
4756
4863
  },
4864
+ recordPipelinePhase(sample) {
4865
+ if (!enabled || sample.signal?.aborted === true) {
4866
+ return false;
4867
+ }
4868
+ pipelinePhaseSamples.push(normalizePipelinePhaseSample(sample));
4869
+ trimHistory(
4870
+ pipelinePhaseSamples,
4871
+ settings.maxRetainedPipelinePhaseSamples
4872
+ );
4873
+ return true;
4874
+ },
4757
4875
  recordFrame(sample) {
4758
4876
  if (!enabled || sample.signal?.aborted === true) {
4759
4877
  return false;
@@ -4795,6 +4913,7 @@ function createGpuDebugSession(options = {}) {
4795
4913
  averageGpuBusyMs: average(gpuBusyTimes)
4796
4914
  },
4797
4915
  dag: buildDagSnapshot(),
4916
+ pipeline: buildPipelineSnapshot(),
4798
4917
  limitations: LIMITATIONS
4799
4918
  };
4800
4919
  return snapshot;
@@ -4806,6 +4925,7 @@ function createGpuDebugSession(options = {}) {
4806
4925
  readyLaneSamples.splice(0, readyLaneSamples.length);
4807
4926
  dispatchSamples.splice(0, dispatchSamples.length);
4808
4927
  dependencyUnlockSamples.splice(0, dependencyUnlockSamples.length);
4928
+ pipelinePhaseSamples.splice(0, pipelinePhaseSamples.length);
4809
4929
  frameSamples.splice(0, frameSamples.length);
4810
4930
  peakTrackedBytes = 0;
4811
4931
  }
@@ -6550,7 +6670,145 @@ function buildDemoDom(root, options) {
6550
6670
  sceneNotes: root.querySelector("#sceneNotes")
6551
6671
  };
6552
6672
  }
6553
- function buildClothSurface(model, state, meshDetail) {
6673
+ function buildSceneSnapshot(state, shipModel) {
6674
+ return Object.freeze({
6675
+ focus: state.focus,
6676
+ frame: state.frame,
6677
+ time: state.time,
6678
+ stress: state.stress,
6679
+ collisions: state.contactCount,
6680
+ collisionCount: state.collisionCount,
6681
+ collisionFlash: state.collisionFlash,
6682
+ sprays: Object.freeze(
6683
+ state.sprays.map(
6684
+ (spray) => Object.freeze({
6685
+ life: spray.life,
6686
+ position: Object.freeze({ ...spray.position }),
6687
+ velocity: Object.freeze({ ...spray.velocity })
6688
+ })
6689
+ )
6690
+ ),
6691
+ ships: Object.freeze(
6692
+ state.ships.map(
6693
+ (ship) => Object.freeze({
6694
+ id: ship.id,
6695
+ position: Object.freeze({ ...ship.position }),
6696
+ velocity: Object.freeze({ ...ship.velocity }),
6697
+ rotationY: ship.rotationY,
6698
+ angularVelocity: ship.angularVelocity,
6699
+ tint: Object.freeze({ ...ship.tint })
6700
+ })
6701
+ )
6702
+ ),
6703
+ waveImpulses: Object.freeze(
6704
+ state.waveImpulses.map(
6705
+ (impulse) => Object.freeze({
6706
+ x: impulse.x,
6707
+ z: impulse.z,
6708
+ strength: impulse.strength,
6709
+ radius: impulse.radius,
6710
+ life: impulse.life
6711
+ })
6712
+ )
6713
+ ),
6714
+ physics: Object.freeze({
6715
+ profile: state.physics.profile,
6716
+ plan: state.physics.plan,
6717
+ manifest: state.physics.manifest,
6718
+ snapshot: state.physics.snapshot,
6719
+ shipPhysics: shipModel?.physics ?? null
6720
+ })
6721
+ });
6722
+ }
6723
+ function resolveSceneDescription(state, options, shipModel) {
6724
+ const scene = buildSceneSnapshot(state, shipModel);
6725
+ if (typeof options.describeState !== "function") {
6726
+ return { scene, description: null };
6727
+ }
6728
+ const description = options.describeState(state.packageState, scene) ?? null;
6729
+ return { scene, description };
6730
+ }
6731
+ function updatePackageState(state, options, shipModel, dt) {
6732
+ if (typeof options.updateState !== "function") {
6733
+ return;
6734
+ }
6735
+ const scene = buildSceneSnapshot(state, shipModel);
6736
+ const nextState = options.updateState(state.packageState, scene, dt);
6737
+ if (typeof nextState !== "undefined") {
6738
+ state.packageState = nextState;
6739
+ }
6740
+ }
6741
+ function normalizeColorOverride(color, fallback) {
6742
+ if (!color || typeof color !== "object") {
6743
+ return fallback;
6744
+ }
6745
+ return {
6746
+ r: typeof color.r === "number" ? color.r : fallback.r,
6747
+ g: typeof color.g === "number" ? color.g : fallback.g,
6748
+ b: typeof color.b === "number" ? color.b : fallback.b
6749
+ };
6750
+ }
6751
+ function readVisualNumber(value, fallback) {
6752
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
6753
+ }
6754
+ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
6755
+ const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6756
+ const defaults = {
6757
+ skyTop: premiumShadows ? "#f0f7fb" : "#e8f1f7",
6758
+ skyMid: premiumShadows ? "#c7d9e5" : "#b9ceda",
6759
+ skyBottom: premiumShadows ? "#84a7bd" : "#7b9bb0",
6760
+ seaTop: premiumShadows ? "#235064" : "#264c5f",
6761
+ seaMid: premiumShadows ? "#153e53" : "#173d4f",
6762
+ seaBottom: "#0b2433",
6763
+ sunCore: "rgba(255, 244, 210, 0.9)",
6764
+ reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
6765
+ shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
6766
+ waveAmplitude: 1,
6767
+ waveDirection: { x: 0.86, z: 0.34 },
6768
+ wavePhaseSpeed: 1,
6769
+ wakeStrength: 0.24,
6770
+ wakeLength: 15,
6771
+ collisionRippleStrength: 0.34,
6772
+ waterNear: { r: 0.12, g: 0.36, b: 0.46 },
6773
+ waterFar: { r: 0.28, g: 0.56, b: 0.68 },
6774
+ harborWall: { r: 0.48, g: 0.4, b: 0.32 },
6775
+ harborDeck: { r: 0.5, g: 0.34, b: 0.22 },
6776
+ harborTower: { r: 0.34, g: 0.32, b: 0.36 },
6777
+ flagColor: { r: 0.76, g: 0.24, b: 0.18 },
6778
+ flagMotion: 1
6779
+ };
6780
+ return {
6781
+ skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
6782
+ skyMid: typeof customVisuals.skyMid === "string" ? customVisuals.skyMid : defaults.skyMid,
6783
+ skyBottom: typeof customVisuals.skyBottom === "string" ? customVisuals.skyBottom : defaults.skyBottom,
6784
+ seaTop: typeof customVisuals.seaTop === "string" ? customVisuals.seaTop : defaults.seaTop,
6785
+ seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
6786
+ seaBottom: typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
6787
+ sunCore: typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.sunCore,
6788
+ reflectionStrength: readVisualNumber(
6789
+ customVisuals.reflectionStrength,
6790
+ defaults.reflectionStrength
6791
+ ),
6792
+ shadowAccent: readVisualNumber(customVisuals.shadowAccent, defaults.shadowAccent),
6793
+ waveAmplitude: readVisualNumber(customVisuals.waveAmplitude, defaults.waveAmplitude),
6794
+ waveDirection: customVisuals.waveDirection && typeof customVisuals.waveDirection.x === "number" && typeof customVisuals.waveDirection.z === "number" ? { x: customVisuals.waveDirection.x, z: customVisuals.waveDirection.z } : defaults.waveDirection,
6795
+ wavePhaseSpeed: readVisualNumber(customVisuals.wavePhaseSpeed, defaults.wavePhaseSpeed),
6796
+ wakeStrength: readVisualNumber(customVisuals.wakeStrength, defaults.wakeStrength),
6797
+ wakeLength: readVisualNumber(customVisuals.wakeLength, defaults.wakeLength),
6798
+ collisionRippleStrength: readVisualNumber(
6799
+ customVisuals.collisionRippleStrength,
6800
+ defaults.collisionRippleStrength
6801
+ ),
6802
+ waterNear: normalizeColorOverride(customVisuals.waterNear, defaults.waterNear),
6803
+ waterFar: normalizeColorOverride(customVisuals.waterFar, defaults.waterFar),
6804
+ harborWall: normalizeColorOverride(customVisuals.harborWall, defaults.harborWall),
6805
+ harborDeck: normalizeColorOverride(customVisuals.harborDeck, defaults.harborDeck),
6806
+ harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
6807
+ flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
6808
+ flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion)
6809
+ };
6810
+ }
6811
+ function buildClothSurface(model, state, meshDetail, visuals) {
6554
6812
  const clothPlan = createClothRepresentationPlan({
6555
6813
  garmentId: "shore-flag",
6556
6814
  kind: state.focus === "cloth" ? "flag" : clothGarmentKinds[0],
@@ -6576,12 +6834,12 @@ function buildClothSurface(model, state, meshDetail) {
6576
6834
  for (let column = 0; column < cols; column += 1) {
6577
6835
  const u = column / (cols - 1);
6578
6836
  const v = row / (rows - 1);
6579
- const gust = Math.sin(time * 1.9 + v * 3.2 + u * 2.1) * continuity.broadMotionFloor;
6580
- const wrinkle = Math.sin(time * 4.4 + u * 9.2 + v * 5.6) * continuity.wrinkleFloor * 0.22;
6837
+ const gust = Math.sin(time * 1.9 + v * 3.2 + u * 2.1) * continuity.broadMotionFloor * visuals.flagMotion;
6838
+ const wrinkle = Math.sin(time * 4.4 + u * 9.2 + v * 5.6) * continuity.wrinkleFloor * 0.22 * Math.max(0.55, visuals.flagMotion);
6581
6839
  const x = origin.x + u * 1.8 + gust * 0.55 * (u * 0.9);
6582
6840
  const y = origin.y - height * v + wrinkle * 0.2;
6583
6841
  const z = origin.z + width * u + gust * 0.72 * (u * 0.85);
6584
- const flap = Math.cos(time * 2.7 + u * 7.4 + v * 3.8) * continuity.broadMotionFloor * 0.28;
6842
+ const flap = Math.cos(time * 2.7 + u * 7.4 + v * 3.8) * continuity.broadMotionFloor * 0.28 * visuals.flagMotion;
6585
6843
  positions.push(vec3(x + flap, y, z));
6586
6844
  }
6587
6845
  }
@@ -6599,15 +6857,74 @@ function buildClothSurface(model, state, meshDetail) {
6599
6857
  band,
6600
6858
  representation,
6601
6859
  continuity,
6860
+ color: visuals.flagColor,
6602
6861
  positions,
6603
6862
  indices,
6604
6863
  grid: { rows, cols }
6605
6864
  };
6606
6865
  }
6607
- function sampleWave(x, z, time) {
6608
- return Math.sin(x * 0.18 + time * 1.2) * 0.55 + Math.cos(z * 0.12 + time * 0.9) * 0.35 + Math.sin((x + z) * 0.08 + time * 1.6) * 0.22;
6866
+ function resolveWaveDirection(state) {
6867
+ const direction = state.demoVisuals?.waveDirection;
6868
+ if (direction && typeof direction === "object" && typeof direction.x === "number" && typeof direction.z === "number") {
6869
+ return normalizeVec3(vec3(direction.x, 0, direction.z));
6870
+ }
6871
+ return normalizeVec3(vec3(0.86, 0, 0.34));
6609
6872
  }
6610
- function buildWaterBands(state, fluidDetail) {
6873
+ function sampleShipWake(state, x, z, time) {
6874
+ const wakeStrength = readVisualNumber(state.demoVisuals?.wakeStrength, 0.24);
6875
+ const wakeLength = readVisualNumber(state.demoVisuals?.wakeLength, 15);
6876
+ let total = 0;
6877
+ for (const ship of state.ships) {
6878
+ const speed = Math.hypot(ship.velocity.x, ship.velocity.z);
6879
+ if (speed <= 0.05) {
6880
+ continue;
6881
+ }
6882
+ const direction = normalizeVec3(vec3(ship.velocity.x, 0, ship.velocity.z));
6883
+ const behind = scaleVec3(direction, -1);
6884
+ const lateral = vec3(-direction.z, 0, direction.x);
6885
+ const delta = vec3(x - ship.position.x, 0, z - ship.position.z);
6886
+ const along = dotVec3(delta, behind);
6887
+ if (along < 0 || along > wakeLength) {
6888
+ continue;
6889
+ }
6890
+ const cross = Math.abs(dotVec3(delta, lateral));
6891
+ const width = 0.9 + along * 0.2;
6892
+ if (cross > width * 3.2) {
6893
+ continue;
6894
+ }
6895
+ const envelope = Math.exp(-along * 0.14) * Math.exp(-(cross * cross / Math.max(0.4, width * width * 2.4)));
6896
+ total += Math.sin(along * 1.6 - time * 4.2) * speed * wakeStrength * envelope;
6897
+ }
6898
+ return total;
6899
+ }
6900
+ function sampleWaveImpulses(state, x, z, time) {
6901
+ const rippleStrength = readVisualNumber(state.demoVisuals?.collisionRippleStrength, 0.34);
6902
+ let total = 0;
6903
+ for (const impulse of state.waveImpulses) {
6904
+ const dx = x - impulse.x;
6905
+ const dz = z - impulse.z;
6906
+ const distance = Math.hypot(dx, dz);
6907
+ const radius = impulse.radius + (1 - impulse.life) * 4.8;
6908
+ if (distance > radius * 2.8) {
6909
+ continue;
6910
+ }
6911
+ const phase = distance * 1.8 - (1 - impulse.life) * 10 - time * 0.4;
6912
+ const envelope = Math.exp(-distance / Math.max(0.1, radius)) * impulse.life;
6913
+ total += Math.sin(phase) * impulse.strength * rippleStrength * envelope * 0.18;
6914
+ }
6915
+ return total;
6916
+ }
6917
+ function sampleWave(state, x, z, time) {
6918
+ const direction = resolveWaveDirection(state);
6919
+ const lateral = vec3(-direction.z, 0, direction.x);
6920
+ const along = x * direction.x + z * direction.z;
6921
+ const cross = x * lateral.x + z * lateral.z;
6922
+ const phaseSpeed = readVisualNumber(state.demoVisuals?.wavePhaseSpeed, 1);
6923
+ const amplitude = readVisualNumber(state.demoVisuals?.waveAmplitude, 1);
6924
+ const base = Math.sin(along * 0.22 - time * 1.12 * phaseSpeed) * 0.42 + Math.cos(along * 0.11 + cross * 0.07 - time * 0.78 * phaseSpeed) * 0.26 + Math.sin(cross * 0.19 - time * 1.34 * phaseSpeed) * 0.16;
6925
+ return base * amplitude + sampleShipWake(state, x, z, time) + sampleWaveImpulses(state, x, z, time);
6926
+ }
6927
+ function buildWaterBands(state, fluidDetail, visuals) {
6611
6928
  const fluidPlan = createFluidRepresentationPlan({
6612
6929
  fluidBodyId: "harbor",
6613
6930
  kind: state.focus === "fluid" ? "ocean" : fluidBodyKinds[0],
@@ -6640,7 +6957,7 @@ function buildWaterBands(state, fluidDetail) {
6640
6957
  const v = row / (rows - 1);
6641
6958
  const x = originX + bandSpec.width * u;
6642
6959
  const z = originZ + bandSpec.depth * v;
6643
- const y = bandSpec.y + sampleWave(x, z, state.time) * continuity.amplitudeFloor * (bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
6960
+ const y = bandSpec.y + sampleWave(state, x, z, state.time) * continuity.amplitudeFloor * (bandSpec.band === "near" ? 0.9 : bandSpec.band === "mid" ? 0.55 : 0.3);
6644
6961
  positions.push(vec3(x, y, z));
6645
6962
  }
6646
6963
  }
@@ -6661,7 +6978,15 @@ function buildWaterBands(state, fluidDetail) {
6661
6978
  cols,
6662
6979
  positions,
6663
6980
  indices,
6664
- color: bandSpec.band === "near" ? { r: 0.12, g: 0.36, b: 0.46 } : bandSpec.band === "mid" ? { r: 0.15, g: 0.42, b: 0.54 } : bandSpec.band === "far" ? { r: 0.22, g: 0.48, b: 0.6 } : { r: 0.34, g: 0.58, b: 0.7 }
6981
+ color: bandSpec.band === "near" ? visuals.waterNear : bandSpec.band === "mid" ? {
6982
+ r: mix(visuals.waterNear.r, visuals.waterFar.r, 0.4),
6983
+ g: mix(visuals.waterNear.g, visuals.waterFar.g, 0.4),
6984
+ b: mix(visuals.waterNear.b, visuals.waterFar.b, 0.4)
6985
+ } : bandSpec.band === "far" ? visuals.waterFar : {
6986
+ r: mix(visuals.waterFar.r, 0.76, 0.2),
6987
+ g: mix(visuals.waterFar.g, 0.78, 0.2),
6988
+ b: mix(visuals.waterFar.b, 0.82, 0.2)
6989
+ }
6665
6990
  });
6666
6991
  }
6667
6992
  return { fluidPlan, bandMeshes };
@@ -6700,6 +7025,9 @@ function createSceneState(options) {
6700
7025
  clothDetail,
6701
7026
  lightingDetail,
6702
7027
  debugSession,
7028
+ packageState: void 0,
7029
+ demoDescription: null,
7030
+ demoVisuals: null,
6703
7031
  time: 0,
6704
7032
  lastTimeMs: null,
6705
7033
  paused: false,
@@ -6727,7 +7055,9 @@ function createSceneState(options) {
6727
7055
  }
6728
7056
  ],
6729
7057
  sprays: [],
7058
+ waveImpulses: [],
6730
7059
  frame: 0,
7060
+ contactCount: 0,
6731
7061
  collisionCount: 0,
6732
7062
  collisionFlash: 0,
6733
7063
  physics: {
@@ -6742,24 +7072,24 @@ function createSceneState(options) {
6742
7072
  function setListContent(element, values) {
6743
7073
  element.innerHTML = values.map((value) => `<li>${value}</li>`).join("");
6744
7074
  }
6745
- function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength) {
7075
+ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
6746
7076
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6747
7077
  const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
6748
- sky.addColorStop(0, premiumShadows ? "#f0f7fb" : "#e8f1f7");
6749
- sky.addColorStop(0.6, premiumShadows ? "#c7d9e5" : "#b9ceda");
6750
- sky.addColorStop(1, premiumShadows ? "#84a7bd" : "#7b9bb0");
7078
+ sky.addColorStop(0, visuals.skyTop);
7079
+ sky.addColorStop(0.6, visuals.skyMid);
7080
+ sky.addColorStop(1, visuals.skyBottom);
6751
7081
  ctx.fillStyle = sky;
6752
7082
  ctx.fillRect(0, 0, canvas.width, canvas.height);
6753
7083
  const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
6754
- shoreline.addColorStop(0, premiumShadows ? "#235064" : "#264c5f");
6755
- shoreline.addColorStop(0.52, premiumShadows ? "#153e53" : "#173d4f");
6756
- shoreline.addColorStop(1, "#0b2433");
7084
+ shoreline.addColorStop(0, visuals.seaTop);
7085
+ shoreline.addColorStop(0.52, visuals.seaMid);
7086
+ shoreline.addColorStop(1, visuals.seaBottom);
6757
7087
  ctx.fillStyle = shoreline;
6758
7088
  ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
6759
7089
  const sunX = mix(canvas.width * 0.16, canvas.width * 0.84, (Math.sin(state.time * 0.12) + 1) * 0.5);
6760
7090
  const sunY = canvas.height * 0.14 + Math.cos(state.time * 0.12) * 22;
6761
7091
  const sun = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 90);
6762
- sun.addColorStop(0, "rgba(255, 244, 210, 0.9)");
7092
+ sun.addColorStop(0, visuals.sunCore);
6763
7093
  sun.addColorStop(1, "rgba(255, 244, 210, 0)");
6764
7094
  ctx.fillStyle = sun;
6765
7095
  ctx.beginPath();
@@ -6835,27 +7165,27 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
6835
7165
  ctx.fill();
6836
7166
  ctx.restore();
6837
7167
  }
6838
- function pushHarborGeometry(camera, viewport, triangles) {
7168
+ function pushHarborGeometry(camera, viewport, triangles, visuals) {
6839
7169
  const harborObjects = [
6840
7170
  {
6841
7171
  position: vec3(-8.2, 1.1, -0.9),
6842
7172
  rotationY: -0.16,
6843
7173
  scale: { x: 5.4, y: 2.4, z: 4.2 },
6844
- color: { r: 0.48, g: 0.4, b: 0.32 },
7174
+ color: visuals.harborWall,
6845
7175
  accent: 0.06
6846
7176
  },
6847
7177
  {
6848
7178
  position: vec3(-5.7, 0.45, 1.4),
6849
7179
  rotationY: -0.08,
6850
7180
  scale: { x: 6.8, y: 0.3, z: 2.1 },
6851
- color: { r: 0.5, g: 0.34, b: 0.22 },
7181
+ color: visuals.harborDeck,
6852
7182
  accent: 0.04
6853
7183
  },
6854
7184
  {
6855
7185
  position: vec3(-10.4, 0.28, 0.8),
6856
7186
  rotationY: 0.22,
6857
7187
  scale: { x: 1.2, y: 0.9, z: 1.2 },
6858
- color: { r: 0.34, g: 0.32, b: 0.36 },
7188
+ color: visuals.harborTower,
6859
7189
  accent: 0.02
6860
7190
  }
6861
7191
  ];
@@ -6935,7 +7265,7 @@ function renderClothAccent(ctx, cloth, camera, viewport) {
6935
7265
  cloth.grid.rows * cloth.grid.cols - 1,
6936
7266
  (cloth.grid.rows - 1) * cloth.grid.cols
6937
7267
  ];
6938
- ctx.fillStyle = "rgba(164, 44, 28, 0.95)";
7268
+ ctx.fillStyle = colorToRgba(cloth.color, 0.95);
6939
7269
  for (const index of borderIndices) {
6940
7270
  const point = projected[index];
6941
7271
  if (!point) {
@@ -6996,12 +7326,13 @@ function updateShips(state, dt, shipModel) {
6996
7326
  const physics = shipModel.physics;
6997
7327
  const halfExtents = physics.halfExtents ?? [1.35, 0.95, 3.9];
6998
7328
  let collided = false;
7329
+ state.contactCount = 0;
6999
7330
  for (const ship of state.ships) {
7000
7331
  ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
7001
7332
  ship.rotationY += ship.angularVelocity * dt;
7002
7333
  ship.velocity = scaleVec3(ship.velocity, 1 - (physics.linearDamping ?? 0.04) * dt);
7003
7334
  ship.angularVelocity *= 1 - (physics.angularDamping ?? 0.08) * dt;
7004
- ship.position.y = sampleWave(ship.position.x, ship.position.z, state.time) * 0.22 + (physics.waterline ?? 0.42);
7335
+ ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.22 + (physics.waterline ?? 0.42);
7005
7336
  if (Math.abs(ship.position.x) > 10) {
7006
7337
  ship.velocity.x *= -1;
7007
7338
  ship.angularVelocity *= -1;
@@ -7028,11 +7359,25 @@ function updateShips(state, dt, shipModel) {
7028
7359
  b.angularVelocity -= 0.55;
7029
7360
  const contactPoint = vec3((a.position.x + b.position.x) * 0.5, (a.position.y + b.position.y) * 0.5 + 0.1, (a.position.z + b.position.z) * 0.5);
7030
7361
  spawnSpray(state, contactPoint, Math.abs(dx) + Math.abs(dz));
7362
+ state.waveImpulses.push({
7363
+ x: contactPoint.x,
7364
+ z: contactPoint.z,
7365
+ strength: Math.min(1.4, 0.2 + (Math.abs(dx) + Math.abs(dz)) * 0.18),
7366
+ radius: 0.8,
7367
+ life: 1
7368
+ });
7031
7369
  state.collisionCount += 1;
7370
+ state.contactCount = 1;
7032
7371
  collided = true;
7033
7372
  }
7034
7373
  state.collisionFlash = collided ? 1 : Math.max(0, state.collisionFlash - dt * 1.8);
7035
7374
  }
7375
+ function updateWaveImpulses(state, dt) {
7376
+ state.waveImpulses = state.waveImpulses.map((impulse) => ({
7377
+ ...impulse,
7378
+ life: impulse.life - dt * 0.55
7379
+ })).filter((impulse) => impulse.life > 0);
7380
+ }
7036
7381
  function updateSpray(state, dt) {
7037
7382
  state.sprays = state.sprays.map((particle) => {
7038
7383
  const nextVelocity = vec3(particle.velocity.x, particle.velocity.y - 4.2 * dt, particle.velocity.z);
@@ -7146,7 +7491,7 @@ function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDi
7146
7491
  vec3(bounds.min[0], keelY, bounds.max[2])
7147
7492
  ].map((point) => transformPoint(point, transform));
7148
7493
  renderProjectedShadow(ctx, hullCorners, camera, viewport, lightDir, {
7149
- planeY: sampleWave(ship.position.x, ship.position.z, state.time) * 0.24 - 0.03,
7494
+ planeY: sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 - 0.03,
7150
7495
  alpha: 0.08 + shadowStrength * 0.2,
7151
7496
  blur: 14 + shadowStrength * 24
7152
7497
  });
@@ -7175,12 +7520,30 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7175
7520
  const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
7176
7521
  const lightDir = normalizeVec3(vec3(-0.45, 0.85, -0.24));
7177
7522
  const lightingSnapshot = state.lightingDetail.getSnapshot();
7178
- const reflectionStrength = lightingSnapshot.currentLevel.config.reflectionStrength;
7179
- const shadowStrength = lightingSnapshot.currentLevel.config.shadowStrength;
7180
- drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength);
7523
+ const visuals = resolveVisualConfig(
7524
+ nearLighting,
7525
+ lightingSnapshot,
7526
+ state.demoDescription?.visuals
7527
+ );
7528
+ state.demoVisuals = visuals;
7529
+ const reflectionStrength = visuals.reflectionStrength;
7530
+ const shadowStrength = visuals.shadowAccent;
7531
+ drawSkyAndShore(
7532
+ ctx,
7533
+ canvas,
7534
+ state,
7535
+ nearLighting,
7536
+ reflectionStrength,
7537
+ shadowStrength,
7538
+ visuals
7539
+ );
7181
7540
  const triangles = [];
7182
- pushHarborGeometry(camera, viewport, triangles);
7183
- const water = buildWaterBands(state, state.fluidDetail.getSnapshot().currentLevel.config);
7541
+ pushHarborGeometry(camera, viewport, triangles, visuals);
7542
+ const water = buildWaterBands(
7543
+ state,
7544
+ state.fluidDetail.getSnapshot().currentLevel.config,
7545
+ visuals
7546
+ );
7184
7547
  for (const bandMesh of water.bandMeshes) {
7185
7548
  const bandAccent = bandMesh.band === "near" ? 0.06 : bandMesh.band === "mid" ? 0.04 : 0;
7186
7549
  for (let index = 0; index < bandMesh.indices.length; index += 3) {
@@ -7202,7 +7565,12 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7202
7565
  });
7203
7566
  }
7204
7567
  }
7205
- const cloth = buildClothSurface(state, state, state.clothDetail.getSnapshot().currentLevel.config);
7568
+ const cloth = buildClothSurface(
7569
+ state,
7570
+ state,
7571
+ state.clothDetail.getSnapshot().currentLevel.config,
7572
+ visuals
7573
+ );
7206
7574
  for (let index = 0; index < cloth.indices.length; index += 3) {
7207
7575
  const a = cloth.positions[cloth.indices[index]];
7208
7576
  const b = cloth.positions[cloth.indices[index + 1]];
@@ -7217,7 +7585,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7217
7585
  depth: (projected[0].depth + projected[1].depth + projected[2].depth) / 3,
7218
7586
  worldCenter: scaleVec3(addVec3(addVec3(a, b), c), 1 / 3),
7219
7587
  normal,
7220
- baseColor: { r: 0.76, g: 0.24, b: 0.18 },
7588
+ baseColor: cloth.color,
7221
7589
  accent: cloth.band === "near" ? 0.1 : 0.04
7222
7590
  });
7223
7591
  }
@@ -7280,15 +7648,26 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7280
7648
  "The ships collide on GLTF-derived hull volumes while cloth and fluid remain downstream visual consumers.",
7281
7649
  "Near-field lighting keeps the ray-traced-primary shadow impression so the collision read stays crisp."
7282
7650
  ] : SCENE_NOTES;
7283
- setListContent(dom.sceneMetrics, sceneMetrics);
7284
- setListContent(dom.qualityMetrics, qualityMetrics);
7285
- setListContent(dom.debugMetrics, debugMetrics);
7286
- setListContent(dom.sceneNotes, sceneNotes);
7287
- dom.status.textContent = `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
7288
- dom.details.textContent = state.focus === "physics" ? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; GLTF ships collide on ${shipModel.physics.shape ?? "box"} volumes while visual follow-up remains downstream.` : `GLTF ships are colliding with ${shipModel.physics.shape ?? "box"} physics volumes; cloth and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
7651
+ const custom = state.demoDescription ?? null;
7652
+ setListContent(
7653
+ dom.sceneMetrics,
7654
+ Array.isArray(custom?.sceneMetrics) ? custom.sceneMetrics : sceneMetrics
7655
+ );
7656
+ setListContent(
7657
+ dom.qualityMetrics,
7658
+ Array.isArray(custom?.qualityMetrics) ? custom.qualityMetrics : qualityMetrics
7659
+ );
7660
+ setListContent(
7661
+ dom.debugMetrics,
7662
+ Array.isArray(custom?.debugMetrics) ? custom.debugMetrics : debugMetrics
7663
+ );
7664
+ setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
7665
+ dom.status.textContent = typeof custom?.status === "string" ? custom.status : `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
7666
+ dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; GLTF ships collide on ${shipModel.physics.shape ?? "box"} volumes while visual follow-up remains downstream.` : `GLTF ships are colliding with ${shipModel.physics.shape ?? "box"} physics volumes; cloth and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
7289
7667
  }
7290
7668
  function updateSceneState(state, dt, shipModel) {
7291
7669
  updateShips(state, dt, shipModel);
7670
+ updateWaveImpulses(state, dt);
7292
7671
  updateSpray(state, dt);
7293
7672
  updatePhysicsSnapshot(state, shipModel);
7294
7673
  }
@@ -7307,13 +7686,15 @@ function syncTextState(state, shipModel) {
7307
7686
  })),
7308
7687
  shipPhysics: shipModel.physics,
7309
7688
  sprays: state.sprays.length,
7689
+ waveImpulses: state.waveImpulses.length,
7310
7690
  pressure: state.lastDecision?.pressureLevel ?? "stable",
7311
7691
  physics: {
7312
7692
  profile: state.physics.profile,
7313
7693
  snapshotStageId: state.physics.plan.snapshotStageId,
7314
7694
  workerJobCount: state.physics.manifest.jobs.length,
7315
7695
  snapshot: state.physics.snapshot
7316
- }
7696
+ },
7697
+ package: state.demoDescription?.textState ?? null
7317
7698
  };
7318
7699
  window.render_game_to_text = () => JSON.stringify(snapshot);
7319
7700
  window.advanceTime = (ms) => {
@@ -7329,6 +7710,9 @@ function syncTextState(state, shipModel) {
7329
7710
  async function mountGpuShowcase(options = {}) {
7330
7711
  injectStyles();
7331
7712
  const root = options.root ?? document.body;
7713
+ const previousMarkup = root.innerHTML;
7714
+ const previousRenderGameToText = window.render_game_to_text;
7715
+ const previousAdvanceTime = window.advanceTime;
7332
7716
  const focus = options.focus ?? new URLSearchParams(window.location.search).get("focus") ?? "integrated";
7333
7717
  const dom = buildDemoDom(root, {
7334
7718
  packageName: options.packageName ?? "@plasius/gpu-demo-viewer",
@@ -7339,11 +7723,18 @@ async function mountGpuShowcase(options = {}) {
7339
7723
  const state = createSceneState({ focus });
7340
7724
  const shipModel = await loadGltfModel(resolveShowcaseAssetUrl());
7341
7725
  state.shipModel = shipModel;
7726
+ state.packageState = typeof options.createState === "function" ? options.createState() : void 0;
7342
7727
  updatePhysicsSnapshot(state, shipModel);
7343
7728
  state.lastDecision = recordTelemetry(state, 16.4);
7729
+ state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7344
7730
  syncTextState(state, shipModel);
7345
7731
  const ctx = dom.canvas.getContext("2d");
7732
+ let destroyed = false;
7733
+ let frameHandle = null;
7346
7734
  const renderFrame = (nowMs) => {
7735
+ if (destroyed) {
7736
+ return;
7737
+ }
7347
7738
  if (!state.paused) {
7348
7739
  if (state.lastTimeMs == null) {
7349
7740
  state.lastTimeMs = nowMs;
@@ -7353,32 +7744,60 @@ async function mountGpuShowcase(options = {}) {
7353
7744
  state.time += dt;
7354
7745
  state.frame += 1;
7355
7746
  updateSceneState(state, dt, shipModel);
7747
+ updatePackageState(state, options, shipModel, dt);
7356
7748
  const syntheticFrame = 14.2 + state.sprays.length * 0.1 + (state.stress ? 6.4 : 0);
7357
7749
  state.lastDecision = recordTelemetry(state, syntheticFrame);
7358
7750
  }
7751
+ state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7359
7752
  renderScene(ctx, dom.canvas, state, shipModel, dom);
7360
7753
  syncTextState(state, shipModel);
7361
- requestAnimationFrame(renderFrame);
7754
+ frameHandle = requestAnimationFrame(renderFrame);
7362
7755
  };
7363
- dom.pauseButton.addEventListener("click", () => {
7756
+ const handlePauseClick = () => {
7364
7757
  state.paused = !state.paused;
7365
7758
  dom.pauseButton.textContent = state.paused ? "Resume" : "Pause";
7366
- });
7367
- dom.stressToggle.addEventListener("change", () => {
7759
+ };
7760
+ const handleStressChange = () => {
7368
7761
  state.stress = dom.stressToggle.checked;
7369
- });
7370
- dom.focusMode.addEventListener("change", () => {
7762
+ };
7763
+ const handleFocusChange = () => {
7371
7764
  state.focus = dom.focusMode.value;
7372
7765
  Object.assign(state.camera, {
7373
7766
  ...CAMERA_PRESETS[state.focus],
7374
7767
  target: vec3(...CAMERA_PRESETS[state.focus].target)
7375
7768
  });
7376
- });
7377
- requestAnimationFrame(renderFrame);
7769
+ };
7770
+ dom.pauseButton.addEventListener("click", handlePauseClick);
7771
+ dom.stressToggle.addEventListener("change", handleStressChange);
7772
+ dom.focusMode.addEventListener("change", handleFocusChange);
7773
+ frameHandle = requestAnimationFrame(renderFrame);
7378
7774
  return {
7379
7775
  state,
7380
7776
  shipModel,
7381
- canvas: dom.canvas
7777
+ canvas: dom.canvas,
7778
+ destroy() {
7779
+ if (destroyed) {
7780
+ return;
7781
+ }
7782
+ destroyed = true;
7783
+ if (frameHandle != null) {
7784
+ cancelAnimationFrame(frameHandle);
7785
+ }
7786
+ dom.pauseButton.removeEventListener("click", handlePauseClick);
7787
+ dom.stressToggle.removeEventListener("change", handleStressChange);
7788
+ dom.focusMode.removeEventListener("change", handleFocusChange);
7789
+ root.innerHTML = previousMarkup;
7790
+ if (typeof previousRenderGameToText === "function") {
7791
+ window.render_game_to_text = previousRenderGameToText;
7792
+ } else {
7793
+ delete window.render_game_to_text;
7794
+ }
7795
+ if (typeof previousAdvanceTime === "function") {
7796
+ window.advanceTime = previousAdvanceTime;
7797
+ } else {
7798
+ delete window.advanceTime;
7799
+ }
7800
+ }
7382
7801
  };
7383
7802
  }
7384
7803
  function updatePhysicsSnapshot(state, shipModel) {
@@ -7395,6 +7814,7 @@ function updatePhysicsSnapshot(state, shipModel) {
7395
7814
  contactCount: state.collisionFlash > 0.02 ? 1 : 0,
7396
7815
  metadata: {
7397
7816
  collisionCount: state.collisionCount,
7817
+ contactCount: state.contactCount,
7398
7818
  snapshotStageId: state.physics.plan.snapshotStageId,
7399
7819
  rigidBodyShape: shipModel.physics.shape ?? "box"
7400
7820
  }
@@ -7405,4 +7825,4 @@ export {
7405
7825
  resolveShowcaseAssetUrl,
7406
7826
  showcaseFocusModes
7407
7827
  };
7408
- //# sourceMappingURL=showcase-runtime-5H44EZXD.js.map
7828
+ //# sourceMappingURL=showcase-runtime-V72XV55N.js.map