@plasius/gpu-shared 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,23 @@ All notable changes to this project will be documented in this file.
16
16
  - **Security**
17
17
  - (placeholder)
18
18
 
19
+ ## [1.0.1] - 2026-06-27
20
+
21
+ - **Added**
22
+ - (placeholder)
23
+
24
+ - **Changed**
25
+ - The shared harbor showcase now keeps the modeled lighthouse, dock, and
26
+ shoreline asset set even when `gpu_showcase_realistic_models_v1` is
27
+ disabled, falling back only to a brigantine-only ship catalog.
28
+
29
+ - **Fixed**
30
+ - Added regression coverage for the flag-disabled modeled-harbor path and
31
+ the bounded legacy fallback used only when showcase asset loading fails.
32
+
33
+ - **Security**
34
+ - (placeholder)
35
+
19
36
  ## [1.0.0] - 2026-06-27
20
37
 
21
38
  - **Added**
@@ -331,3 +348,4 @@ All notable changes to this project will be documented in this file.
331
348
  [0.1.19]: https://github.com/Plasius-LTD/gpu-shared/releases/tag/v0.1.19
332
349
  [0.1.20]: https://github.com/Plasius-LTD/gpu-shared/releases/tag/v0.1.20
333
350
  [1.0.0]: https://github.com/Plasius-LTD/gpu-shared/releases/tag/v1.0.0
351
+ [1.0.1]: https://github.com/Plasius-LTD/gpu-shared/releases/tag/v1.0.1
package/README.md CHANGED
@@ -209,6 +209,10 @@ distinct cutter profile, a modeled lighthouse, a dock/warehouse scene, and a
209
209
  generated rocky shoreline/breakwater so the harbor reads closer to a believable
210
210
  coastal night view on high-end machines.
211
211
 
212
+ If `gpu_showcase_realistic_models_v1` is disabled, the harbor keeps the modeled
213
+ lighthouse/dock/shoreline environment and falls back only to a brigantine-only
214
+ ship catalog instead of reverting to placeholder harbor blocks.
215
+
212
216
  For slide-deck screenshots or video capture, open the route with
213
217
  `?capture=1&renderScale=1`. Capture mode hides the validation chrome, fills the
214
218
  viewport with the scene canvas, and caps the backing buffer at 1080p by default
@@ -11,7 +11,7 @@ var gpuSharedEnGbTranslations = Object.freeze({
11
11
  "gpuShared.showcase.details.booting": "Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.",
12
12
  "gpuShared.showcase.details.physics": "Stable world snapshots are emitted from {snapshotStageId} after the authoritative solver; the heavier hull now carries momentum through mass-aware collision impulses while cloth and fluid remain downstream.",
13
13
  "gpuShared.showcase.details.realistic": "Moonlit GLTF ships now mix a brigantine and a cutter against modeled harbor assets; cloth, fluid, and ship-local lighting stay continuous while the governor pressure is {pressureLevel}.",
14
- "gpuShared.showcase.details.legacy": "Moonlit GLTF ships use the legacy brigantine and placeholder harbor blocks while cloth, fluid, and ship-local lighting stay continuous while the governor pressure is {pressureLevel}.",
14
+ "gpuShared.showcase.details.legacy": "Showcase fallback keeps the brigantine-only harbor path active while cloth, fluid, and ship-local lighting stay continuous under {pressureLevel} governor pressure.",
15
15
  "gpuShared.showcase.action.pause": "Pause",
16
16
  "gpuShared.showcase.action.resume": "Resume",
17
17
  "gpuShared.showcase.control.stressMode": "Stress mode",
@@ -116,4 +116,4 @@ export {
116
116
  translateGpuSharedText,
117
117
  createGpuSharedTranslator
118
118
  };
119
- //# sourceMappingURL=chunk-CH3ZS5TQ.js.map
119
+ //# sourceMappingURL=chunk-Z6SOXBHL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/feature-flags.js","../src/translations/en-GB.js","../src/i18n.js"],"sourcesContent":["export const GPU_SHOWCASE_REALISTIC_MODELS_FEATURE = \"gpu_showcase_realistic_models_v1\";\nexport const GPU_SHOWCASE_PRODUCT_STUDIO_FEATURE = \"gpu_showcase_product_studio_wavefront_v1\";\n","export const gpuSharedEnGbTranslations = Object.freeze({\n \"gpuShared.showcase.title\": \"Flag by the Sea\",\n \"gpuShared.showcase.subtitle\":\n \"Shared 3D validation scene using GLTF ships, cloth, fluid continuity, adaptive performance, and telemetry.\",\n \"gpuShared.showcase.status.booting\": \"Booting 3D scene...\",\n \"gpuShared.showcase.status.live\": \"3D scene live - {fps} FPS\",\n \"gpuShared.showcase.details.booting\":\n \"Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.\",\n \"gpuShared.showcase.details.physics\":\n \"Stable world snapshots are emitted from {snapshotStageId} after the authoritative solver; the heavier hull now carries momentum through mass-aware collision impulses while cloth and fluid remain downstream.\",\n \"gpuShared.showcase.details.realistic\":\n \"Moonlit GLTF ships now mix a brigantine and a cutter against modeled harbor assets; cloth, fluid, and ship-local lighting stay continuous while the governor pressure is {pressureLevel}.\",\n \"gpuShared.showcase.details.legacy\":\n \"Showcase fallback keeps the brigantine-only harbor path active while cloth, fluid, and ship-local lighting stay continuous under {pressureLevel} governor pressure.\",\n \"gpuShared.showcase.action.pause\": \"Pause\",\n \"gpuShared.showcase.action.resume\": \"Resume\",\n \"gpuShared.showcase.control.stressMode\": \"Stress mode\",\n \"gpuShared.showcase.control.focus\": \"Focus\",\n \"gpuShared.showcase.focus.integrated\": \"integrated\",\n \"gpuShared.showcase.focus.lighting\": \"lighting\",\n \"gpuShared.showcase.focus.cloth\": \"cloth\",\n \"gpuShared.showcase.focus.fluid\": \"fluid\",\n \"gpuShared.showcase.focus.physics\": \"physics\",\n \"gpuShared.showcase.focus.performance\": \"performance\",\n \"gpuShared.showcase.focus.debug\": \"debug\",\n \"gpuShared.showcase.legend.title\": \"Scene\",\n \"gpuShared.showcase.legend.shipMetadata\":\n \"GLTF ships carry hull mass and damping metadata.\",\n \"gpuShared.showcase.legend.lighting\":\n \"Lanterns and torches warm the moonlit harbor.\",\n \"gpuShared.showcase.legend.collisions\":\n \"Mass-aware collisions stay authoritative near the camera.\",\n \"gpuShared.showcase.section.sceneState\": \"Scene State\",\n \"gpuShared.showcase.section.qualityBudgets\": \"Quality + Budgets\",\n \"gpuShared.showcase.section.debugTelemetry\": \"Debug Telemetry\",\n \"gpuShared.showcase.section.notes\": \"Notes\",\n \"gpuShared.showcase.note.assetLoading\":\n \"Ships are loaded from a GLTF asset and carry mass, damping, restitution, and hull extents from node extras.\",\n \"gpuShared.showcase.note.moonlight\":\n \"Moonlight sets the cold ambient read while deck lanterns and harbor torches provide warm local contrast.\",\n \"gpuShared.showcase.note.continuity\":\n \"Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands even in the darker night palette.\",\n \"gpuShared.showcase.note.performance\":\n \"Performance pressure reduces visual detail before mass-weighted authoritative collision motion is touched.\",\n \"gpuShared.showcase.note.physicsSnapshots\":\n \"Stable world snapshots are taken after the authoritative rigid-body commit and before visual follow-up work.\",\n \"gpuShared.showcase.note.physicsCollisions\":\n \"The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.\",\n \"gpuShared.showcase.note.physicsLighting\":\n \"Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water.\",\n \"gpuShared.debug.adapter.showcase\": \"3D showcase\",\n \"gpuShared.debug.allocation.mainColorBuffer\": \"Main color buffer\",\n \"gpuShared.debug.allocation.shadowImpressionAtlas\": \"Shadow impression atlas\",\n});\n","import { gpuSharedEnGbTranslations } from \"./translations/en-GB.js\";\n\nexport const gpuSharedTranslationKeys = Object.freeze({\n showcaseTitle: \"gpuShared.showcase.title\",\n showcaseSubtitle: \"gpuShared.showcase.subtitle\",\n statusBooting: \"gpuShared.showcase.status.booting\",\n statusLive: \"gpuShared.showcase.status.live\",\n detailsBooting: \"gpuShared.showcase.details.booting\",\n detailsPhysics: \"gpuShared.showcase.details.physics\",\n detailsRealistic: \"gpuShared.showcase.details.realistic\",\n detailsLegacy: \"gpuShared.showcase.details.legacy\",\n pause: \"gpuShared.showcase.action.pause\",\n resume: \"gpuShared.showcase.action.resume\",\n stressMode: \"gpuShared.showcase.control.stressMode\",\n focus: \"gpuShared.showcase.control.focus\",\n focusIntegrated: \"gpuShared.showcase.focus.integrated\",\n focusLighting: \"gpuShared.showcase.focus.lighting\",\n focusCloth: \"gpuShared.showcase.focus.cloth\",\n focusFluid: \"gpuShared.showcase.focus.fluid\",\n focusPhysics: \"gpuShared.showcase.focus.physics\",\n focusPerformance: \"gpuShared.showcase.focus.performance\",\n focusDebug: \"gpuShared.showcase.focus.debug\",\n legendTitle: \"gpuShared.showcase.legend.title\",\n legendShipMetadata: \"gpuShared.showcase.legend.shipMetadata\",\n legendLighting: \"gpuShared.showcase.legend.lighting\",\n legendCollisions: \"gpuShared.showcase.legend.collisions\",\n sceneState: \"gpuShared.showcase.section.sceneState\",\n qualityBudgets: \"gpuShared.showcase.section.qualityBudgets\",\n debugTelemetry: \"gpuShared.showcase.section.debugTelemetry\",\n notes: \"gpuShared.showcase.section.notes\",\n noteAssetLoading: \"gpuShared.showcase.note.assetLoading\",\n noteMoonlight: \"gpuShared.showcase.note.moonlight\",\n noteContinuity: \"gpuShared.showcase.note.continuity\",\n notePerformance: \"gpuShared.showcase.note.performance\",\n notePhysicsSnapshots: \"gpuShared.showcase.note.physicsSnapshots\",\n notePhysicsCollisions: \"gpuShared.showcase.note.physicsCollisions\",\n notePhysicsLighting: \"gpuShared.showcase.note.physicsLighting\",\n debugAdapterShowcase: \"gpuShared.debug.adapter.showcase\",\n debugMainColorBuffer: \"gpuShared.debug.allocation.mainColorBuffer\",\n debugShadowImpressionAtlas: \"gpuShared.debug.allocation.shadowImpressionAtlas\",\n});\n\nexport const gpuSharedTranslations = Object.freeze({\n \"en-GB\": gpuSharedEnGbTranslations,\n});\n\nfunction formatTranslation(template, args = {}) {\n return template.replace(/\\{([A-Za-z0-9_]+)\\}/g, (match, name) => {\n if (!Object.prototype.hasOwnProperty.call(args, name)) {\n return match;\n }\n\n const value = args[name];\n return value == null ? \"\" : String(value);\n });\n}\n\nexport function translateGpuSharedText(key, args, translate) {\n const translated = translate?.(key, args);\n if (translated && translated !== key) {\n return translated;\n }\n\n const fallback = gpuSharedEnGbTranslations[key];\n return fallback ? formatTranslation(fallback, args) : key;\n}\n\nexport function createGpuSharedTranslator(translate) {\n return (key, args) => translateGpuSharedText(key, args, translate);\n}\n\n"],"mappings":";AAAO,IAAM,wCAAwC;AAC9C,IAAM,sCAAsC;;;ACD5C,IAAM,4BAA4B,OAAO,OAAO;AAAA,EACrD,4BAA4B;AAAA,EAC5B,+BACE;AAAA,EACF,qCAAqC;AAAA,EACrC,kCAAkC;AAAA,EAClC,sCACE;AAAA,EACF,sCACE;AAAA,EACF,wCACE;AAAA,EACF,qCACE;AAAA,EACF,mCAAmC;AAAA,EACnC,oCAAoC;AAAA,EACpC,yCAAyC;AAAA,EACzC,oCAAoC;AAAA,EACpC,uCAAuC;AAAA,EACvC,qCAAqC;AAAA,EACrC,kCAAkC;AAAA,EAClC,kCAAkC;AAAA,EAClC,oCAAoC;AAAA,EACpC,wCAAwC;AAAA,EACxC,kCAAkC;AAAA,EAClC,mCAAmC;AAAA,EACnC,0CACE;AAAA,EACF,sCACE;AAAA,EACF,wCACE;AAAA,EACF,yCAAyC;AAAA,EACzC,6CAA6C;AAAA,EAC7C,6CAA6C;AAAA,EAC7C,oCAAoC;AAAA,EACpC,wCACE;AAAA,EACF,qCACE;AAAA,EACF,sCACE;AAAA,EACF,uCACE;AAAA,EACF,4CACE;AAAA,EACF,6CACE;AAAA,EACF,2CACE;AAAA,EACF,oCAAoC;AAAA,EACpC,8CAA8C;AAAA,EAC9C,oDAAoD;AACtD,CAAC;;;ACnDM,IAAM,2BAA2B,OAAO,OAAO;AAAA,EACpD,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,uBAAuB;AAAA,EACvB,qBAAqB;AAAA,EACrB,sBAAsB;AAAA,EACtB,sBAAsB;AAAA,EACtB,4BAA4B;AAC9B,CAAC;AAEM,IAAM,wBAAwB,OAAO,OAAO;AAAA,EACjD,SAAS;AACX,CAAC;AAED,SAAS,kBAAkB,UAAU,OAAO,CAAC,GAAG;AAC9C,SAAO,SAAS,QAAQ,wBAAwB,CAAC,OAAO,SAAS;AAC/D,QAAI,CAAC,OAAO,UAAU,eAAe,KAAK,MAAM,IAAI,GAAG;AACrD,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,KAAK,IAAI;AACvB,WAAO,SAAS,OAAO,KAAK,OAAO,KAAK;AAAA,EAC1C,CAAC;AACH;AAEO,SAAS,uBAAuB,KAAK,MAAM,WAAW;AAC3D,QAAM,aAAa,YAAY,KAAK,IAAI;AACxC,MAAI,cAAc,eAAe,KAAK;AACpC,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,0BAA0B,GAAG;AAC9C,SAAO,WAAW,kBAAkB,UAAU,IAAI,IAAI;AACxD;AAEO,SAAS,0BAA0B,WAAW;AACnD,SAAO,CAAC,KAAK,SAAS,uBAAuB,KAAK,MAAM,SAAS;AACnE;","names":[]}
package/dist/index.cjs CHANGED
@@ -137,7 +137,7 @@ var init_en_GB = __esm({
137
137
  "gpuShared.showcase.details.booting": "Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.",
138
138
  "gpuShared.showcase.details.physics": "Stable world snapshots are emitted from {snapshotStageId} after the authoritative solver; the heavier hull now carries momentum through mass-aware collision impulses while cloth and fluid remain downstream.",
139
139
  "gpuShared.showcase.details.realistic": "Moonlit GLTF ships now mix a brigantine and a cutter against modeled harbor assets; cloth, fluid, and ship-local lighting stay continuous while the governor pressure is {pressureLevel}.",
140
- "gpuShared.showcase.details.legacy": "Moonlit GLTF ships use the legacy brigantine and placeholder harbor blocks while cloth, fluid, and ship-local lighting stay continuous while the governor pressure is {pressureLevel}.",
140
+ "gpuShared.showcase.details.legacy": "Showcase fallback keeps the brigantine-only harbor path active while cloth, fluid, and ship-local lighting stay continuous under {pressureLevel} governor pressure.",
141
141
  "gpuShared.showcase.action.pause": "Pause",
142
142
  "gpuShared.showcase.action.resume": "Resume",
143
143
  "gpuShared.showcase.control.stressMode": "Stress mode",
@@ -4080,42 +4080,74 @@ function buildTrianglesFromMesh(mesh, transform, colorOverride, camera, viewport
4080
4080
  }
4081
4081
  }
4082
4082
  }
4083
- async function loadShowcaseAssetCatalog() {
4084
- const [brigantine, cutter, lighthouse, harborDock, shoreline] = await Promise.all([
4083
+ function createShowcaseAssetCatalog({
4084
+ mode,
4085
+ ships,
4086
+ environment,
4087
+ primaryShipKey = "brigantine",
4088
+ fallbackReason = null
4089
+ }) {
4090
+ return Object.freeze({
4091
+ mode,
4092
+ primaryShipKey,
4093
+ ships: Object.freeze(ships),
4094
+ environment: Object.freeze(environment),
4095
+ fallbackReason
4096
+ });
4097
+ }
4098
+ function normalizeAssetCatalogFailureReason(error) {
4099
+ if (typeof error?.message === "string" && error.message.trim().length > 0) {
4100
+ return error.message;
4101
+ }
4102
+ return "showcase asset loading failed";
4103
+ }
4104
+ async function loadShowcaseAssetCatalog({ includeSecondaryShip = true } = {}) {
4105
+ const [brigantine, lighthouse, harborDock, shoreline] = await Promise.all([
4085
4106
  loadGltfModel(resolveShowcaseAssetUrl("brigantine")),
4086
- loadGltfModel(resolveShowcaseAssetUrl("cutter")),
4087
4107
  loadGltfModel(resolveShowcaseAssetUrl("lighthouse")),
4088
4108
  loadGltfModel(resolveShowcaseAssetUrl("harbor-dock")),
4089
4109
  loadGltfModel(resolveShowcaseAssetUrl("shoreline"))
4090
4110
  ]);
4091
- return Object.freeze({
4092
- primaryShipKey: "brigantine",
4093
- ships: Object.freeze({
4094
- brigantine,
4095
- cutter
4096
- }),
4097
- environment: Object.freeze({
4111
+ const ships = {
4112
+ brigantine
4113
+ };
4114
+ if (includeSecondaryShip) {
4115
+ ships.cutter = await loadGltfModel(resolveShowcaseAssetUrl("cutter"));
4116
+ }
4117
+ return createShowcaseAssetCatalog({
4118
+ mode: includeSecondaryShip ? "modeled-rich" : "modeled-baseline",
4119
+ ships,
4120
+ environment: {
4098
4121
  lighthouse,
4099
4122
  "harbor-dock": harborDock,
4100
4123
  shoreline
4101
- })
4124
+ }
4102
4125
  });
4103
4126
  }
4104
- function createLegacyShowcaseAssetCatalog() {
4105
- const brigantine = loadGltfModel(resolveShowcaseAssetUrl("brigantine"));
4106
- return Promise.resolve(brigantine).then(
4107
- (primary) => Object.freeze({
4108
- primaryShipKey: "brigantine",
4109
- ships: Object.freeze({
4110
- brigantine: primary
4111
- }),
4112
- environment: Object.freeze({})
4113
- })
4114
- );
4127
+ async function createLegacyShowcaseAssetCatalog(error = null) {
4128
+ const brigantine = await loadGltfModel(resolveShowcaseAssetUrl("brigantine"));
4129
+ return createShowcaseAssetCatalog({
4130
+ mode: "legacy-fallback",
4131
+ ships: {
4132
+ brigantine
4133
+ },
4134
+ environment: {},
4135
+ fallbackReason: normalizeAssetCatalogFailureReason(error)
4136
+ });
4137
+ }
4138
+ async function loadShowcaseAssetCatalogWithFallback({ includeSecondaryShip = true } = {}) {
4139
+ try {
4140
+ return await loadShowcaseAssetCatalog({ includeSecondaryShip });
4141
+ } catch (error) {
4142
+ return createLegacyShowcaseAssetCatalog(error);
4143
+ }
4115
4144
  }
4116
4145
  function resolveShipModel(state, ship, fallbackModel = null) {
4117
4146
  return state.assetCatalog?.ships?.[ship.modelKey ?? state.assetCatalog?.primaryShipKey ?? "brigantine"] ?? fallbackModel ?? state.shipModel;
4118
4147
  }
4148
+ function hasModeledHarborEnvironment(state) {
4149
+ return Object.keys(state.assetCatalog?.environment ?? {}).length > 0;
4150
+ }
4119
4151
  function createPerformanceGovernor(performanceFeatures) {
4120
4152
  const createQualityLadderAdapter = assertRequiredFunction(
4121
4153
  performanceFeatures,
@@ -5268,7 +5300,7 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
5268
5300
  ctx.restore();
5269
5301
  }
5270
5302
  function pushHarborGeometry(camera, viewport, triangles, state) {
5271
- if (!state.showcaseRealisticModelsEnabled) {
5303
+ if (!hasModeledHarborEnvironment(state)) {
5272
5304
  for (const object of LEGACY_HARBOR_LAYOUT) {
5273
5305
  buildTrianglesFromMesh(
5274
5306
  { positions: [object], indices: [0], normals: null, colors: null, material: createLegacyMeshPrimitive({})?.material, bounds: null, name: "legacy-structure" },
@@ -6024,7 +6056,7 @@ function renderLighthouseBeam(ctx, state, camera, viewport, visuals) {
6024
6056
  const lighthousePlacement = SHOWCASE_ENVIRONMENT_LAYOUT.find(
6025
6057
  (placement) => placement.assetKey === "lighthouse"
6026
6058
  );
6027
- if (!lighthousePlacement || !state.showcaseRealisticModelsEnabled) {
6059
+ if (!lighthousePlacement || !state.showcaseRealisticModelsEnabled || !hasModeledHarborEnvironment(state)) {
6028
6060
  return;
6029
6061
  }
6030
6062
  const source = transformPoint2(
@@ -6360,7 +6392,7 @@ function renderScene(ctx, canvas, state, shipModel, dom, lightingFeatures, fluid
6360
6392
  };
6361
6393
  const sceneMetrics = [
6362
6394
  `focus: ${state.focus}`,
6363
- `ships: ${state.ships.length} active GLTF hulls across ${new Set(state.ships.map((ship) => ship.modelKey)).size} model families`,
6395
+ `ships: ${state.ships.length} active GLTF hulls across ${new Set(state.ships.map((ship) => resolveShipModel(state, ship, shipModel)?.name ?? ship.modelKey)).size} model families`,
6364
6396
  `moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.reduce((total, ship) => total + (Array.isArray(ship.lanterns) ? ship.lanterns.length : 0), 0)} warm deck and harbor lights`,
6365
6397
  `physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
6366
6398
  `physics contacts: ${state.contactCount}`,
@@ -6438,17 +6470,28 @@ function syncTextState(state, shipModel, featureAdapters) {
6438
6470
  coordinateSystem: "right-handed world; +x right, +y up, +z forward from the shore",
6439
6471
  focus: state.focus,
6440
6472
  stress: state.stress,
6441
- ships: state.ships.map((ship) => ({
6442
- id: ship.id,
6443
- modelKey: ship.modelKey ?? "brigantine",
6444
- x: Number(ship.position.x.toFixed(2)),
6445
- y: Number(ship.position.y.toFixed(2)),
6446
- z: Number(ship.position.z.toFixed(2)),
6447
- vx: Number(ship.velocity.x.toFixed(2)),
6448
- vz: Number(ship.velocity.z.toFixed(2)),
6449
- massKg: Math.round(getShipMass(ship, resolveShipModel(state, ship, shipModel))),
6450
- lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
6451
- })),
6473
+ ships: state.ships.map((ship) => {
6474
+ const resolvedShipModel = resolveShipModel(state, ship, shipModel);
6475
+ return {
6476
+ id: ship.id,
6477
+ modelKey: ship.modelKey ?? "brigantine",
6478
+ resolvedModelKey: resolvedShipModel?.name ?? ship.modelKey ?? "brigantine",
6479
+ x: Number(ship.position.x.toFixed(2)),
6480
+ y: Number(ship.position.y.toFixed(2)),
6481
+ z: Number(ship.position.z.toFixed(2)),
6482
+ vx: Number(ship.velocity.x.toFixed(2)),
6483
+ vz: Number(ship.velocity.z.toFixed(2)),
6484
+ massKg: Math.round(getShipMass(ship, resolvedShipModel)),
6485
+ lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
6486
+ };
6487
+ }),
6488
+ assetCatalog: {
6489
+ mode: state.assetCatalog?.mode ?? "unknown",
6490
+ shipKeys: Object.keys(state.assetCatalog?.ships ?? {}).sort(),
6491
+ environmentKeys: Object.keys(state.assetCatalog?.environment ?? {}).sort(),
6492
+ fallbackReason: state.assetCatalog?.fallbackReason ?? null,
6493
+ requestedRealisticModels: state.showcaseRealisticModelsEnabled
6494
+ },
6452
6495
  shipPhysics: Object.fromEntries(
6453
6496
  state.ships.map((ship) => [ship.id, resolveShipModel(state, ship, shipModel)?.physics ?? null])
6454
6497
  ),
@@ -6505,7 +6548,9 @@ async function mountGpuShowcase(options = {}, featureFlags = null) {
6505
6548
  },
6506
6549
  featureAdapters
6507
6550
  );
6508
- const assetCatalog = await (state.showcaseRealisticModelsEnabled ? loadShowcaseAssetCatalog() : createLegacyShowcaseAssetCatalog());
6551
+ const assetCatalog = await loadShowcaseAssetCatalogWithFallback({
6552
+ includeSecondaryShip: state.showcaseRealisticModelsEnabled
6553
+ });
6509
6554
  const shipModel = assetCatalog.ships[assetCatalog.primaryShipKey];
6510
6555
  state.assetCatalog = assetCatalog;
6511
6556
  state.shipModel = shipModel;