[object Object]

← back to Quadrille Showroom

Slice-3 H: globally-unique element build codes (1000QH<version>)

1b5eb937a623b655fdfc9aa18b0bad202d6fffa1 · 2026-06-28 18:46:17 -0700 · Steve Abrams

assignElementCodes() runs once at registry load — iterates VERSION_ORDER, each
version's elements by ascending n, handing out base-1000 global seq across all 11
builds; stamps elt.code (e.g. seq 0 + QH + V2 -> 1000QHV2). Deterministic + stable.
BUILD_CODE='QH' isolated to one swappable constant. Pin identity, Chosen tray, and
Copy-as-text now key on the durable global code, not the version-local 'V1.4' key
(which collided across versions). Pins show the code chip + friendly badge n.
qh_chosen localStorage migrates old key-only rows (backfills code, never crashes).

Files touched

Diff

commit 1b5eb937a623b655fdfc9aa18b0bad202d6fffa1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jun 28 18:46:17 2026 -0700

    Slice-3 H: globally-unique element build codes (1000QH<version>)
    
    assignElementCodes() runs once at registry load — iterates VERSION_ORDER, each
    version's elements by ascending n, handing out base-1000 global seq across all 11
    builds; stamps elt.code (e.g. seq 0 + QH + V2 -> 1000QHV2). Deterministic + stable.
    BUILD_CODE='QH' isolated to one swappable constant. Pin identity, Chosen tray, and
    Copy-as-text now key on the durable global code, not the version-local 'V1.4' key
    (which collided across versions). Pins show the code chip + friendly badge n.
    qh_chosen localStorage migrates old key-only rows (backfills code, never crashes).
---
 public/css/showroom.css |  21 +++++--
 public/js/versions.js   | 144 +++++++++++++++++++++++++++++++++++++++---------
 2 files changed, 134 insertions(+), 31 deletions(-)

diff --git a/public/css/showroom.css b/public/css/showroom.css
index 8a0bd6a..6ef5695 100644
--- a/public/css/showroom.css
+++ b/public/css/showroom.css
@@ -486,12 +486,24 @@ body { overflow: hidden; background: #0a0a0f; font-family: 'Segoe UI', system-ui
   transition: transform .12s, background .12s, box-shadow .12s;
 }
 .el-pin:hover .pin-badge { transform: scale(1.12); box-shadow: 0 4px 16px rgba(0,0,0,0.6), 0 0 0 4px rgba(201,169,110,0.30); }
+/* GLOBALLY-UNIQUE element code (1003QHV1) — ALWAYS visible on a numbered pin so
+   every element shows its durable cross-build ID, alongside the friendly badge n. */
+.el-pin .pin-code {
+  display: inline-flex; align-items: center; white-space: nowrap;
+  margin-left: 5px; padding: 3px 7px; border-radius: 7px;
+  font-family: ui-monospace, "SF Mono", Menlo, monospace;
+  font-size: 10.5px; font-weight: 700; letter-spacing: .4px; line-height: 1;
+  color: #f0e6cf; background: rgba(14,12,18,0.86);
+  border: 1px solid rgba(201,169,110,0.40);
+  box-shadow: 0 2px 8px rgba(0,0,0,0.45);
+}
+.el-pin.chosen .pin-code { color: #1a1206; background: linear-gradient(180deg,#d8bd84,#b9933f); border-color: #fff0d2; }
 .el-pin .pin-label {
   max-width: 0; overflow: hidden; white-space: nowrap;
   font-size: 12px; font-weight: 600; letter-spacing: .3px; color: #f5efdc;
   background: rgba(14,12,18,0.86); border: 1px solid rgba(201,169,110,0.30);
-  border-left: none; border-radius: 0 9px 9px 0; line-height: 1;
-  margin-left: -4px; padding: 0; opacity: 0;
+  border-radius: 9px; line-height: 1;
+  margin-left: 5px; padding: 0; opacity: 0;
   transition: max-width .18s ease, opacity .15s ease, padding .18s ease;
 }
 .el-pin:hover .pin-label, .el-pin.show-label .pin-label {
@@ -546,8 +558,9 @@ body { overflow: hidden; background: #0a0a0f; font-family: 'Segoe UI', system-ui
   background: rgba(255,255,255,0.05); border: 1px solid rgba(201,169,110,0.26);
 }
 .ct-chip .ctc-tag {
-  font-size: 11px; font-weight: 800; letter-spacing: .5px; color: #c9a96e;
-  font-variant-numeric: tabular-nums; min-width: 42px;
+  font-family: ui-monospace, "SF Mono", Menlo, monospace;
+  font-size: 10.5px; font-weight: 800; letter-spacing: .4px; color: #c9a96e;
+  font-variant-numeric: tabular-nums; flex: 0 0 auto; white-space: nowrap;
 }
 .ct-chip .ctc-name {
   flex: 1; font-size: 12.5px; color: #ede4cf; line-height: 1.2;
diff --git a/public/js/versions.js b/public/js/versions.js
index fcccf02..a44018a 100644
--- a/public/js/versions.js
+++ b/public/js/versions.js
@@ -340,16 +340,80 @@ function boot() {
   };
   const VERSION_ORDER = ['V1','V2','V3','V4','V5','V6','V7','V8','V9','V10','V11'];
 
+  // ===========================================================================
+  // GLOBALLY-UNIQUE ELEMENT CODES (Slice-3 / chunk H)
+  // ---------------------------------------------------------------------------
+  // Steve's rule: every numbered element gets a GLOBALLY-UNIQUE build code that
+  // is unique across ALL 11 builds, base 1000, carrying the build code + the
+  // element's version. Canonical example: element 1 → `1000QHV2` =
+  //   `${1000 + globalSeq}` + `${BUILD_CODE}` + `${version}`.
+  //
+  // BUILD_CODE is ONE swappable constant. Steve typed "build code CH" but his
+  // concrete worked example is `1000QH…`, so we use 'QH' per the literal example
+  // and isolate it here — flip to 'CH' is a single-char change if Steve confirms.
+  const BUILD_CODE = 'QH';
+  //
+  // assignElementCodes() runs ONCE at registry load: it iterates VERSION_ORDER
+  // (deterministic, stable order) → each version's `elements` by ASCENDING `n`,
+  // handing out global sequence 1000, 1001, 1002 … and stamping `elt.code` on
+  // every element. Because VERSION_ORDER and each elements[] array are fixed
+  // literals, the same element ALWAYS gets the same code on every page load,
+  // independent of click order or which version is opened.
+  //   code = `${1000 + globalSeq}${BUILD_CODE}${version}`  e.g. seq 0, V2 → 1000QHV2
+  function assignElementCodes() {
+    let globalSeq = 0;
+    VERSION_ORDER.forEach(vk => {
+      const v = VERSIONS[vk];
+      if (!v || !Array.isArray(v.elements)) return;
+      // Ascending-n is the friendly index order; elements are authored in n-order
+      // already, but sort defensively so a future out-of-order edit can't shuffle codes.
+      v.elements.slice().sort((a, b) => a.n - b.n).forEach(elt => {
+        elt.code = '' + (1000 + globalSeq) + BUILD_CODE + vk;
+        globalSeq++;
+      });
+    });
+  }
+  assignElementCodes();
+
+  // Look up an element's code from its version + n (for migrating old chosen rows
+  // and for cross-checks). Returns null if the element no longer exists.
+  function codeFor(version, n) {
+    const v = VERSIONS[version];
+    if (!v || !Array.isArray(v.elements)) return null;
+    const elt = v.elements.find(e => e.n === n);
+    return elt ? elt.code : null;
+  }
+
   // ===========================================================================
   // STATE
   // ===========================================================================
   let currentVersion = null;
   let lastThreeDVersion = 'V1';       // last in-engine version (so Back from a mechanic restores a real scene)
   let overlayOn = false;
-  let chosen = loadChosen();          // [{ key:'V1.4', version:'V1', n:4, label:'…' }]
+  let chosen = loadChosen();          // [{ code:'1003QHV1', version:'V1', n:4, label:'…' }]
 
+  // IDENTITY (Slice-3 / chunk H): a chosen element is keyed by its globally-unique
+  // `code` (e.g. 1003QHV1), NOT the old version-local 'V1.4' key (which collided
+  // across versions). loadChosen() tolerates OLD localStorage rows that only carried
+  // `key` — it backfills `code` from {version,n} where the element still exists, and
+  // keeps any it can't resolve so an old pick stays displayable instead of crashing.
   function loadChosen() {
-    try { return JSON.parse(localStorage.getItem('qh_chosen') || '[]'); } catch (e) { return []; }
+    let raw;
+    try { raw = JSON.parse(localStorage.getItem('qh_chosen') || '[]'); } catch (e) { return []; }
+    if (!Array.isArray(raw)) return [];
+    return raw.map(c => {
+      if (!c || typeof c !== 'object') return null;
+      if (c.code) return c;                       // already migrated
+      // Old shape: derive version/n from {version,n} or a 'V1.4' key, then backfill code.
+      let version = c.version, n = c.n;
+      if ((version == null || n == null) && typeof c.key === 'string' && c.key.indexOf('.') > 0) {
+        const parts = c.key.split('.');
+        version = version != null ? version : parts[0];
+        n = n != null ? n : parseInt(parts[1], 10);
+      }
+      const code = (version != null && n != null) ? (codeFor(version, n) || c.key || ('?' + (c.key||''))) : (c.key || '?');
+      return { code, version: version != null ? version : null, n: n != null ? n : null, label: c.label || '' };
+    }).filter(Boolean);
   }
   function saveChosen() {
     try { localStorage.setItem('qh_chosen', JSON.stringify(chosen)); } catch (e) {}
@@ -443,16 +507,19 @@ function boot() {
       const back3d = lastThreeDVersion || 'V1';
       setVersion(back3d);
     } else if (d.type === 'choose') {
-      const key = d.version + '.' + d.n;
-      const exists = isChosen(key);
-      if (d.chosen && !exists) { chosen.push({ key, version: d.version, n: d.n, label: d.label }); bumpCount(); }
-      else if (!d.chosen && exists) { const i = chosen.findIndex(c => c.key === key); if (i >= 0) chosen.splice(i, 1); }
+      // Protos still send {version,n}; resolve to the global code (overlay mechanics'
+      // legends key locally by n, but the Chosen tray + copy use the durable code).
+      const code = codeFor(d.version, d.n) || (d.version + '.' + d.n);
+      const exists = isChosen(code);
+      if (d.chosen && !exists) { chosen.push({ code, version: d.version, n: d.n, label: d.label }); bumpCount(); }
+      else if (!d.chosen && exists) { const i = chosen.findIndex(c => c.code === code); if (i >= 0) chosen.splice(i, 1); }
       saveChosen(); renderTray();
     } else if (d.type === 'ready') {
-      // sync the current chosen set for THIS version into the proto's legend
+      // sync the current chosen set for THIS version into the proto's legend. The proto
+      // legend keys by the local 'V_.n' id, so map our chosen rows back to that shape.
       const frame = document.getElementById('mech-frame');
       if (frame && frame.contentWindow) {
-        const keys = chosen.filter(c => c.version === d.version).map(c => c.key);
+        const keys = chosen.filter(c => c.version === d.version).map(c => c.version + '.' + c.n);
         try { frame.contentWindow.postMessage({ __qhPicker: true, type: 'sync-chosen', keys }, '*'); } catch (e) {}
       }
     }
@@ -462,7 +529,7 @@ function boot() {
   // NUMBERED-ELEMENT OVERLAY — circled-number pins projected onto each element.
   // ===========================================================================
   let pinLayer = null;
-  let activePins = [];   // [{ el, anchor, n, label, key }]
+  let activePins = [];   // [{ el, anchor, n, label, code }]
 
   function anchorWorld(anchor) {
     if (!anchor) return null;
@@ -484,17 +551,21 @@ function boot() {
     if (!pinLayer) { pinLayer = document.getElementById('pin-layer'); }
     if (!pinLayer) return;
     v.elements.forEach(elt => {
-      const key = currentVersion + '.' + elt.n;
+      // IDENTITY = the globally-unique code (1003QHV1…), NOT the old version-local
+      // 'V1.4' key. The friendly circled number `n` stays on the badge; the code
+      // rides in a sub-label so every visible pin shows its durable cross-build ID.
+      const code = elt.code || (currentVersion + '.' + elt.n);   // codeFor-equivalent (already assigned)
       const pin = document.createElement('div');
       pin.className = 'el-pin';
-      pin.dataset.key = key;
+      pin.dataset.code = code;
       pin.innerHTML =
         '<span class="pin-badge">' + elt.n + '</span>' +
+        '<span class="pin-code" title="Globally-unique element code">' + escapeHtml(code) + '</span>' +
         '<span class="pin-label">' + escapeHtml(elt.label) + '</span>';
-      if (isChosen(key)) pin.classList.add('chosen');
-      pin.addEventListener('click', (e) => { e.stopPropagation(); toggleChoice(key, currentVersion, elt.n, elt.label, pin); });
+      if (isChosen(code)) pin.classList.add('chosen');
+      pin.addEventListener('click', (e) => { e.stopPropagation(); toggleChoice(code, currentVersion, elt.n, elt.label, pin); });
       pinLayer.appendChild(pin);
-      activePins.push({ el: pin, anchor: elt.anchor, n: elt.n, label: elt.label, key });
+      activePins.push({ el: pin, anchor: elt.anchor, n: elt.n, label: elt.label, code });
     });
     positionPins(); // place immediately
   }
@@ -547,18 +618,19 @@ function boot() {
   // ===========================================================================
   // CHOSEN ELEMENTS TRAY
   // ===========================================================================
-  function isChosen(key) { return chosen.some(c => c.key === key); }
-  function toggleChoice(key, version, n, label, pinEl) {
-    const i = chosen.findIndex(c => c.key === key);
+  // Chosen-element identity is the globally-unique `code` (1003QHV1…).
+  function isChosen(code) { return chosen.some(c => c.code === code); }
+  function toggleChoice(code, version, n, label, pinEl) {
+    const i = chosen.findIndex(c => c.code === code);
     if (i >= 0) { chosen.splice(i, 1); if (pinEl) pinEl.classList.remove('chosen'); }
-    else { chosen.push({ key, version, n, label }); if (pinEl) pinEl.classList.add('chosen'); bumpCount(); }
+    else { chosen.push({ code, version, n, label }); if (pinEl) pinEl.classList.add('chosen'); bumpCount(); }
     saveChosen(); renderTray();
   }
-  function removeChoice(key) {
-    const i = chosen.findIndex(c => c.key === key);
+  function removeChoice(code) {
+    const i = chosen.findIndex(c => c.code === code);
     if (i >= 0) chosen.splice(i, 1);
     saveChosen(); renderTray();
-    const pin = document.querySelector('#pin-layer .el-pin[data-key="' + cssEsc(key) + '"]');
+    const pin = document.querySelector('#pin-layer .el-pin[data-code="' + cssEsc(code) + '"]');
     if (pin) pin.classList.remove('chosen');
   }
   function bumpCount() {
@@ -566,10 +638,12 @@ function boot() {
     c.classList.add('bump'); setTimeout(() => c.classList.remove('bump'), 180);
   }
   function chosenText() {
-    // Sort by version order then element number for a tidy composition spec.
+    // Sort by version order then element number for a tidy composition spec. Each row
+    // leads with the unambiguous GLOBAL CODE (1003QHV1) so a composed spec references
+    // the durable cross-build ID, not the old "V1.4" version-local key.
     const sorted = chosen.slice().sort((a, b) =>
-      (VERSION_ORDER.indexOf(a.version) - VERSION_ORDER.indexOf(b.version)) || (a.n - b.n));
-    return sorted.map(c => c.key + ' ' + c.label).join('\n');
+      (VERSION_ORDER.indexOf(a.version) - VERSION_ORDER.indexOf(b.version)) || ((a.n || 0) - (b.n || 0)));
+    return sorted.map(c => (c.code || ('V' + c.version + '.' + c.n)) + ' ' + c.label).join('\n');
   }
   function renderTray() {
     const tray = document.getElementById('chosen-tray'); if (!tray) return;
@@ -588,15 +662,16 @@ function boot() {
     if (empty) empty.style.display = 'none';
     items.style.display = 'flex';
     const sorted = chosen.slice().sort((a, b) =>
-      (VERSION_ORDER.indexOf(a.version) - VERSION_ORDER.indexOf(b.version)) || (a.n - b.n));
+      (VERSION_ORDER.indexOf(a.version) - VERSION_ORDER.indexOf(b.version)) || ((a.n || 0) - (b.n || 0)));
     sorted.forEach(c => {
+      const code = c.code || ('V' + c.version + '.' + c.n);
       const row = document.createElement('div');
       row.className = 'ct-chip';
       row.innerHTML =
-        '<span class="ctc-tag">' + c.key + '</span>' +
+        '<span class="ctc-tag" title="Globally-unique element code">' + escapeHtml(code) + '</span>' +
         '<span class="ctc-name">' + escapeHtml(c.label) + '</span>' +
         '<button class="ctc-x" title="Remove">&times;</button>';
-      row.querySelector('.ctc-x').addEventListener('click', () => removeChoice(c.key));
+      row.querySelector('.ctc-x').addEventListener('click', () => removeChoice(code));
       items.appendChild(row);
     });
   }
@@ -734,6 +809,21 @@ function boot() {
     get overlayOn() { return overlayOn; },
     get chosen() { return chosen.slice(); },
     chosenText,
+    // chunk-H surface: the global-code machinery, for the proof harness to assert
+    // uniqueness + stability and for any consumer needing a code lookup.
+    BUILD_CODE,
+    codeFor,
+    assignElementCodes,
+    // Flat list of every {version,n,code,label} across all builds — convenient for
+    // the uniqueness assertion.
+    allCodes() {
+      const out = [];
+      VERSION_ORDER.forEach(vk => {
+        const v = VERSIONS[vk];
+        if (v && Array.isArray(v.elements)) v.elements.forEach(e => out.push({ version: vk, n: e.n, code: e.code, label: e.label }));
+      });
+      return out;
+    },
     VERSIONS, VERSION_ORDER
   };
 }

← c4a14bb Slice-2 contrarian fixes: HOLE-3 detail starts COLLAPSED on  ·  back to Quadrille Showroom  ·  Slice-3 F: FPS.ms.draws HUD readout + off-thread wall-clad d 0979b2b →