Skip to content

Commit 95892c5

Browse files
author
DavidQ
committed
Add Samples↔Tools return-path foundation
- added `Tool` filter to `samples/index.html` - updated `samples/index.render.js` to filter by tool and preselect from `?tool=<toolId>` - updated `tools/renderToolsIndex.js` to compute sample counts from metadata and render `Samples (x)` links - tagged sample `1208` with `toolHints: ["parallax-editor"]` - updated `MASTER_ROADMAP_SAMPLES2TOOLS.md` return-path checklist/status to reflect completed work
1 parent ef8c684 commit 95892c5

5 files changed

Lines changed: 121 additions & 24 deletions

File tree

docs/dev/roadmaps/MASTER_ROADMAP_SAMPLES2TOOLS.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@
4747
- render status indicating source sample
4848

4949
## Return Path (Tools -> Samples)
50-
- [ ] Add a `Tool` filter in Samples.
51-
- [ ] In Tools, add a link label pattern: `Samples (x)` where `x` is count for that tool.
52-
- [ ] Tools `Samples (x)` link passes tool argument back to Samples page.
53-
- [ ] Samples page prepopulates the `Tool` dropdown from query argument and applies filter automatically.
54-
- [ ] Samples return path filter uses `Tool` only (exclude `Workspace Manager` from this filter lane).
50+
- [x] Add a `Tool` filter in Samples.
51+
- [x] In Tools, add a link label pattern: `Samples (x)` where `x` is count for that tool.
52+
- [x] Tools `Samples (x)` link passes tool argument back to Samples page.
53+
- [x] Samples page prepopulates the `Tool` dropdown from query argument and applies filter automatically.
54+
- [x] Samples return path filter uses `Tool` only (exclude `Workspace Manager` from this filter lane).
5555

5656
## Rollout Plan
5757

@@ -106,7 +106,10 @@
106106
- [ ] 3D Camera Path Editor (47 candidates; prioritize camera/path-centric samples)
107107
- [ ] Select precise, semantically aligned samples only (avoid broad keyword-only linkage).
108108

109-
### Phase 5 - Phase 20 Decommission (After Parity)
109+
### Phase 5 - Games
110+
- [ ] Do the same thing for games.
111+
112+
### Phase 6 - Phase 20 Decommission (After Parity)
110113
- [ ] Keep `Phase 20 - Tool Preset Integration` active until Samples2Tools parity is execution-validated.
111114
- [ ] Define parity gate:
112115
- sample-to-tool launch coverage equals or exceeds current Phase 20 matrix intent.
@@ -136,6 +139,6 @@
136139
- [ ] `docs/dev/reports/samples2tools_link_map_<n>.json`
137140

138141
## Current Snapshot (from tools_used.txt)
139-
- [x] Current tagged samples across active tools: `0` (excluding Phase 20)
142+
- [x] Current tagged samples across active tools: `1` (excluding Phase 20)
140143
- [x] Candidate coverage inventory exists for all active tools
141144
- [.] Convert candidates into curated, validated sample-to-tool links

samples/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ <h2>Filter Samples</h2>
4747
<option value="">All classes</option>
4848
</select>
4949
</div>
50+
<div class="samples-filter-field">
51+
<label for="samples-filter-tool">Tool</label>
52+
<select id="samples-filter-tool">
53+
<option value="">All tools</option>
54+
</select>
55+
</div>
5056
<div class="samples-filter-field">
5157
<label for="samples-filter-tag">Tag</label>
5258
<select id="samples-filter-tag">

samples/index.render.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getToolRegistry } from "../tools/toolRegistry.js";
2+
13
const METADATA_PATH = "./metadata/samples.index.metadata.json";
24
const PINNED_KEY = "samples-index-pinned";
35

@@ -69,7 +71,18 @@ function buildClassTokens(classValues, engineClassesUsed) {
6971
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
7072
}
7173

72-
function buildSampleRows(metadata, pinnedSet) {
74+
function buildToolTokens(toolHints, toolLabelMap) {
75+
const deduped = [...new Set(asArray(toolHints).map((entry) => normalizeToken(entry)).filter(Boolean))];
76+
return deduped
77+
.filter((toolId) => toolId !== "workspace-manager")
78+
.map((toolId) => ({
79+
value: toolId,
80+
label: toolLabelMap.get(toolId) || toolId
81+
}))
82+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
83+
}
84+
85+
function buildSampleRows(metadata, pinnedSet, toolLabelMap) {
7386
const phaseInfoMap = new Map(
7487
asArray(metadata?.phases)
7588
.map((phase) => {
@@ -102,6 +115,7 @@ function buildSampleRows(metadata, pinnedSet) {
102115
const href = normalize(sample?.href) || `./phase-${phase}/${id}/index.html`;
103116
const tags = asArray(sample?.tags).map((tag) => normalizeTag(tag)).filter(Boolean);
104117
const classTokens = buildClassTokens(sample?.classValues, sample?.engineClassesUsed);
118+
const toolTokens = buildToolTokens(sample?.toolHints, toolLabelMap);
105119
const previewSrc = normalize(sample?.thumbnail) || normalize(sample?.preview) || "";
106120
return {
107121
id,
@@ -113,6 +127,7 @@ function buildSampleRows(metadata, pinnedSet) {
113127
href,
114128
tags,
115129
classTokens,
130+
toolTokens,
116131
previewSrc,
117132
pinned: pinnedSet.has(id)
118133
};
@@ -134,9 +149,14 @@ function buildSampleRows(metadata, pinnedSet) {
134149
).entries()]
135150
.map(([value, label]) => ({ value, label }))
136151
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
152+
const tools = [...new Map(
153+
sampleRows.flatMap((sample) => sample.toolTokens).map((token) => [token.value, token.label])
154+
).entries()]
155+
.map(([value, label]) => ({ value, label }))
156+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
137157
const tags = [...new Set(sampleRows.flatMap((sample) => sample.tags))].sort();
138158

139-
return { sampleRows, phases, phaseOptions, classes, tags, phaseInfoMap };
159+
return { sampleRows, phases, phaseOptions, classes, tools, tags, phaseInfoMap };
140160
}
141161

142162
function filterSampleRows(sampleRows, filterState) {
@@ -148,14 +168,18 @@ function filterSampleRows(sampleRows, filterState) {
148168
if (filterState.className && !sample.classTokens.some((token) => token.value === filterState.className)) {
149169
return false;
150170
}
171+
if (filterState.toolId && !sample.toolTokens.some((token) => token.value === filterState.toolId)) {
172+
return false;
173+
}
151174
if (filterState.tag && !sample.tags.includes(filterState.tag)) {
152175
return false;
153176
}
154177
if (!query) {
155178
return true;
156179
}
157180
const classText = sample.classTokens.map((token) => `${token.label} ${token.value}`).join(" ");
158-
const haystack = `${sample.phase} ${sample.phaseTitle} ${sample.id} ${sample.title} ${sample.description} ${sample.tags.join(" ")} ${classText}`.toLowerCase();
181+
const toolText = sample.toolTokens.map((token) => `${token.label} ${token.value}`).join(" ");
182+
const haystack = `${sample.phase} ${sample.phaseTitle} ${sample.id} ${sample.title} ${sample.description} ${sample.tags.join(" ")} ${classText} ${toolText}`.toLowerCase();
159183
return haystack.includes(query);
160184
});
161185
}
@@ -300,10 +324,11 @@ export async function initSamplesIndex() {
300324
const pinnedContainer = document.getElementById("samples-pinned-list");
301325
const phaseSelect = document.getElementById("samples-filter-phase");
302326
const classSelect = document.getElementById("samples-filter-class");
327+
const toolSelect = document.getElementById("samples-filter-tool");
303328
const tagSelect = document.getElementById("samples-filter-tag");
304329
const searchInput = document.getElementById("samples-phase-filter-input");
305330
const statusNode = document.getElementById("samples-phase-filter-status");
306-
if (!listContainer || !pinnedContainer || !phaseSelect || !classSelect || !tagSelect || !searchInput || !statusNode) {
331+
if (!listContainer || !pinnedContainer || !phaseSelect || !classSelect || !toolSelect || !tagSelect || !searchInput || !statusNode) {
307332
return;
308333
}
309334

@@ -314,8 +339,14 @@ export async function initSamplesIndex() {
314339
}
315340
const metadata = await response.json();
316341
let pinnedSet = readPinnedSet();
342+
const toolLabelMap = new Map(
343+
getToolRegistry()
344+
.filter((tool) => tool.id !== "workspace-manager")
345+
.map((tool) => [normalizeToken(tool.id), normalize(tool.displayName) || normalize(tool.name) || normalize(tool.id)])
346+
.filter((entry) => entry[0] && entry[1])
347+
);
317348

318-
const model = buildSampleRows(metadata, pinnedSet);
349+
const model = buildSampleRows(metadata, pinnedSet, toolLabelMap);
319350
setSelectOptions(phaseSelect, model.phaseOptions.map((entry) => entry.value), (value) => {
320351
const found = model.phaseOptions.find((entry) => entry.value === value);
321352
return found?.label || `Phase ${value}`;
@@ -324,13 +355,22 @@ export async function initSamplesIndex() {
324355
const found = model.classes.find((entry) => entry.value === value);
325356
return found?.label || value.split("/").at(-1) || value;
326357
});
358+
setSelectOptions(toolSelect, model.tools.map((entry) => entry.value), (value) => {
359+
const found = model.tools.find((entry) => entry.value === value);
360+
return found?.label || value;
361+
});
327362
setSelectOptions(tagSelect, model.tags, (value) => value);
363+
const toolQuery = normalizeToken(new URLSearchParams(window.location.search).get("tool"));
364+
if (toolQuery && model.tools.some((entry) => entry.value === toolQuery)) {
365+
toolSelect.value = toolQuery;
366+
}
328367

329368
const render = () => {
330-
const nextModel = buildSampleRows(metadata, pinnedSet);
369+
const nextModel = buildSampleRows(metadata, pinnedSet, toolLabelMap);
331370
const filterState = {
332371
phase: normalize(phaseSelect.value),
333372
className: normalize(classSelect.value),
373+
toolId: normalizeToken(toolSelect.value),
334374
tag: normalize(tagSelect.value),
335375
query: normalize(searchInput.value)
336376
};
@@ -365,6 +405,7 @@ export async function initSamplesIndex() {
365405

366406
phaseSelect.addEventListener("change", render);
367407
classSelect.addEventListener("change", render);
408+
toolSelect.addEventListener("change", render);
368409
tagSelect.addEventListener("change", render);
369410
searchInput.addEventListener("input", render);
370411

samples/metadata/samples.index.metadata.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6382,6 +6382,9 @@
63826382
"engine/tilemap/index/resolveRectVsTilemap",
63836383
"engine/tilemap/index/Tilemap",
63846384
"engine/utils/index/clamp"
6385+
],
6386+
"toolHints": [
6387+
"parallax-editor"
63856388
]
63866389
},
63876390
{

tools/renderToolsIndex.js

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { getToolRegistry } from "./toolRegistry.js";
22
import { escapeHtml } from "../src/shared/string/stringUtil.js";
33

4+
const SAMPLES_INDEX_PATH = "/samples/index.html";
5+
const SAMPLES_METADATA_PATH = "/samples/metadata/samples.index.metadata.json";
6+
47
function toStandaloneHref(entryPoint) {
58
const normalized = String(entryPoint || "").replace(/^\.?\/*/, "");
69
return normalized ? `/tools/${normalized}` : "#";
@@ -17,10 +20,17 @@ function buildDocumentationLinks(tool) {
1720
];
1821
}
1922

20-
function buildCardLinks(tool) {
23+
function buildCardLinks(tool, sampleCount) {
2124
const docs = buildDocumentationLinks(tool);
25+
const links = [...docs];
26+
if (Number.isInteger(sampleCount) && sampleCount > 0) {
27+
links.push({
28+
label: `Samples (${sampleCount})`,
29+
path: `${SAMPLES_INDEX_PATH}?tool=${encodeURIComponent(tool.id)}`
30+
});
31+
}
2232
const seen = new Set();
23-
return docs.filter((entry) => {
33+
return links.filter((entry) => {
2434
const label = String(entry?.label || "").trim();
2535
const path = String(entry?.path || "").trim();
2636
if (!label || !path) {
@@ -35,9 +45,10 @@ function buildCardLinks(tool) {
3545
});
3646
}
3747

38-
function renderToolCard(tool) {
48+
function renderToolCard(tool, sampleCountByToolId) {
3949
const standaloneHref = toStandaloneHref(tool.entryPoint);
40-
const cardLinks = buildCardLinks(tool);
50+
const sampleCount = Number(sampleCountByToolId.get(tool.id) || 0);
51+
const cardLinks = buildCardLinks(tool, sampleCount);
4152
const sampleLinks = cardLinks.length > 0
4253
? `
4354
<div class="meta">
@@ -109,7 +120,35 @@ function renderWorkspaceManagerSection() {
109120
grid.innerHTML = renderWorkspaceManagerCard();
110121
}
111122

112-
function renderActiveToolsList() {
123+
async function loadSampleCountByToolId() {
124+
const counts = new Map();
125+
try {
126+
const response = await fetch(SAMPLES_METADATA_PATH, { cache: "no-store" });
127+
if (!response.ok) {
128+
return counts;
129+
}
130+
const metadata = await response.json();
131+
const samples = Array.isArray(metadata?.samples) ? metadata.samples : [];
132+
for (const sample of samples) {
133+
if (String(sample?.phase || "").trim() === "20") {
134+
continue;
135+
}
136+
const toolHints = Array.isArray(sample?.toolHints) ? sample.toolHints : [];
137+
for (const rawToolId of toolHints) {
138+
const toolId = String(rawToolId || "").trim().toLowerCase();
139+
if (!toolId || toolId === "workspace-manager") {
140+
continue;
141+
}
142+
counts.set(toolId, Number(counts.get(toolId) || 0) + 1);
143+
}
144+
}
145+
return counts;
146+
} catch {
147+
return counts;
148+
}
149+
}
150+
151+
function renderActiveToolsList(sampleCountByToolId) {
113152
const editorsGrid = document.querySelector("[data-active-tools-editors-grid]");
114153
const utilitiesGrid = document.querySelector("[data-active-tools-utilities-grid]");
115154
const viewersGrid = document.querySelector("[data-active-tools-viewers-grid]");
@@ -122,9 +161,9 @@ function renderActiveToolsList() {
122161
.filter((entry) => entry.id !== "state-inspector")
123162
.sort((left, right) => String(left.displayName || "").localeCompare(String(right.displayName || "")));
124163

125-
const editors = tools.filter((tool) => classifyToolGroup(tool.id) === "editors").map((tool) => renderToolCard(tool));
126-
const utilities = tools.filter((tool) => classifyToolGroup(tool.id) === "utilities").map((tool) => renderToolCard(tool));
127-
const viewers = tools.filter((tool) => classifyToolGroup(tool.id) === "viewers").map((tool) => renderToolCard(tool));
164+
const editors = tools.filter((tool) => classifyToolGroup(tool.id) === "editors").map((tool) => renderToolCard(tool, sampleCountByToolId));
165+
const utilities = tools.filter((tool) => classifyToolGroup(tool.id) === "utilities").map((tool) => renderToolCard(tool, sampleCountByToolId));
166+
const viewers = tools.filter((tool) => classifyToolGroup(tool.id) === "viewers").map((tool) => renderToolCard(tool, sampleCountByToolId));
128167

129168
editorsGrid.innerHTML = editors.join("\n");
130169
utilitiesGrid.innerHTML = utilities.join("\n");
@@ -146,6 +185,11 @@ function sortPlannedCardsAlphabetically() {
146185
.forEach((card) => grid.appendChild(card));
147186
}
148187

149-
renderWorkspaceManagerSection();
150-
renderActiveToolsList();
151-
sortPlannedCardsAlphabetically();
188+
async function initToolsIndex() {
189+
const sampleCountByToolId = await loadSampleCountByToolId();
190+
renderWorkspaceManagerSection();
191+
renderActiveToolsList(sampleCountByToolId);
192+
sortPlannedCardsAlphabetically();
193+
}
194+
195+
void initToolsIndex();

0 commit comments

Comments
 (0)