Skip to content

Commit 250884d

Browse files
author
Delta Team
committed
Merge PR_26177_DELTA_004-hitboxes-real-object-source
2 parents a7c1c98 + b9e91ef commit 250884d

10 files changed

Lines changed: 1125 additions & 437 deletions
Lines changed: 270 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,292 @@
1-
const FOUNDATION_MILESTONES = Object.freeze([
2-
Object.freeze({
3-
milestone: "Tool shell",
4-
notes: "Theme V2 page shell is available from the toolbox.",
5-
status: "Ready",
6-
}),
7-
Object.freeze({
8-
milestone: "Local API contract",
9-
notes: "Placeholder service boundary is reserved for later persistence work.",
10-
status: "Placeholder",
11-
}),
12-
Object.freeze({
13-
milestone: "Editor",
14-
notes: "Drawing and editing start in a later focused PR.",
15-
status: "Deferred",
16-
}),
17-
]);
1+
import { createServerRepositoryClient } from "../../../../src/api/server-api-client.js";
2+
3+
const EMPTY_STATE = "Create an Object and assign a sprite or vector before editing hitboxes.";
4+
const DEFAULT_BOUNDS = Object.freeze({ height: 64, width: 64, x: 0, y: 0 });
5+
const DEFAULT_ORIGIN = Object.freeze({ x: 0, y: 0 });
186

197
const elements = {
20-
contractCount: document.querySelector("[data-hitboxes-contract-count]"),
21-
foundationStatus: document.querySelector("[data-hitboxes-foundation-status]"),
8+
boundingBox: document.querySelector("[data-hitboxes-bounding-box]"),
9+
list: document.querySelector("[data-hitboxes-object-list]"),
2210
log: document.querySelector("[data-hitboxes-log]"),
23-
mode: document.querySelector("[data-hitboxes-mode]"),
24-
owner: document.querySelector("[data-hitboxes-owner]"),
25-
persistence: document.querySelector("[data-hitboxes-persistence]"),
26-
scopeCount: document.querySelector("[data-hitboxes-scope-count]"),
27-
service: document.querySelector("[data-hitboxes-service]"),
28-
table: document.querySelector("[data-hitboxes-foundation-table]"),
29-
toolState: document.querySelector("[data-hitboxes-tool-state]"),
11+
metaBounds: document.querySelector("[data-hitboxes-meta-bounds]"),
12+
metaKey: document.querySelector("[data-hitboxes-meta-key]"),
13+
metaName: document.querySelector("[data-hitboxes-meta-name]"),
14+
metaOrigin: document.querySelector("[data-hitboxes-meta-origin]"),
15+
metaVisual: document.querySelector("[data-hitboxes-meta-visual]"),
16+
objectCount: document.querySelector("[data-hitboxes-object-count]"),
17+
originMarker: document.querySelector("[data-hitboxes-origin-marker]"),
18+
previewFallback: document.querySelector("[data-hitboxes-preview-fallback]"),
19+
previewFrame: document.querySelector("[data-hitboxes-preview-frame]"),
20+
previewImage: document.querySelector("[data-hitboxes-preview-image]"),
21+
previewSummary: document.querySelector("[data-hitboxes-preview-summary]"),
22+
previewTitle: document.querySelector("[data-hitboxes-preview-title]"),
23+
selectedKey: document.querySelector("[data-hitboxes-selected-key]"),
24+
sourceStatus: document.querySelector("[data-hitboxes-source-status]"),
25+
visualType: document.querySelector("[data-hitboxes-visual-type]"),
26+
};
27+
28+
const objectsRepository = createServerRepositoryClient("objects");
29+
const state = {
30+
eligibleObjects: [],
31+
selectedKey: "",
3032
};
3133

34+
function normalizeText(value) {
35+
return String(value || "").trim();
36+
}
37+
38+
function objectKey(object = {}) {
39+
return normalizeText(object.id || object.key || object.objectKey || object.name)
40+
.toLowerCase()
41+
.replace(/[^a-z0-9]+/g, "-")
42+
.replace(/^-+|-+$/g, "");
43+
}
44+
3245
function setText(element, value) {
3346
if (element) {
3447
element.textContent = value;
3548
}
3649
}
3750

38-
function createCell(text) {
51+
function finiteNumber(value, fallback) {
52+
return Number.isFinite(value) ? value : fallback;
53+
}
54+
55+
function normalizeBounds(source = {}) {
56+
const bounds = source.bounds && typeof source.bounds === "object" ? source.bounds : {};
57+
const size = source.size && typeof source.size === "object" ? source.size : {};
58+
return Object.freeze({
59+
height: Math.max(1, finiteNumber(bounds.height ?? size.height, DEFAULT_BOUNDS.height)),
60+
width: Math.max(1, finiteNumber(bounds.width ?? size.width, DEFAULT_BOUNDS.width)),
61+
x: finiteNumber(bounds.x ?? source.x, DEFAULT_BOUNDS.x),
62+
y: finiteNumber(bounds.y ?? source.y, DEFAULT_BOUNDS.y),
63+
});
64+
}
65+
66+
function normalizeOrigin(source = {}) {
67+
const origin = source.origin && typeof source.origin === "object"
68+
? source.origin
69+
: source.objectOrigin && typeof source.objectOrigin === "object"
70+
? source.objectOrigin
71+
: DEFAULT_ORIGIN;
72+
return Object.freeze({
73+
x: finiteNumber(origin.x, DEFAULT_ORIGIN.x),
74+
y: finiteNumber(origin.y, DEFAULT_ORIGIN.y),
75+
});
76+
}
77+
78+
function visualMetadataForObject(object = {}) {
79+
const render = object.render && typeof object.render === "object" ? object.render : {};
80+
const assignedVisual = object.assignedVisualAsset && typeof object.assignedVisualAsset === "object" ? object.assignedVisualAsset : {};
81+
const visualAsset = object.visualAsset && typeof object.visualAsset === "object" ? object.visualAsset : {};
82+
const candidates = [render, assignedVisual, visualAsset, object];
83+
const assetKey = candidates.map((candidate) => normalizeText(candidate.assetKey || candidate.assetId || candidate.id)).find(Boolean) || "";
84+
const previewPath = candidates.map((candidate) => normalizeText(candidate.previewPath || candidate.path || candidate.storedPath || candidate.imagePath)).find(Boolean) || "";
85+
const type = candidates.map((candidate) => normalizeText(candidate.type || candidate.assetType || candidate.kind)).find(Boolean) || "";
86+
const label = candidates.map((candidate) => normalizeText(candidate.label || candidate.name || candidate.assetName)).find(Boolean) || assetKey;
87+
88+
if (!assetKey && !previewPath) {
89+
return null;
90+
}
91+
92+
return Object.freeze({
93+
assetKey,
94+
label: label || assetKey || "Assigned visual asset",
95+
previewPath,
96+
type: type || "Asset",
97+
});
98+
}
99+
100+
function eligibleObjectFromSource(object = {}) {
101+
const visual = visualMetadataForObject(object);
102+
if (!visual) {
103+
return null;
104+
}
105+
const key = objectKey(object);
106+
if (!key) {
107+
return null;
108+
}
109+
return Object.freeze({
110+
bounds: normalizeBounds(object),
111+
key,
112+
name: normalizeText(object.name) || key,
113+
origin: normalizeOrigin(object),
114+
source: Object.freeze({ ...object }),
115+
visual,
116+
});
117+
}
118+
119+
function tableMessage(text) {
120+
const row = document.createElement("tr");
121+
const cell = document.createElement("td");
122+
cell.colSpan = 3;
123+
cell.textContent = text;
124+
row.append(cell);
125+
return row;
126+
}
127+
128+
function tableCell(text) {
39129
const cell = document.createElement("td");
40130
cell.textContent = text;
41131
return cell;
42132
}
43133

44-
function createRow(milestone) {
134+
function visualLabel(visual) {
135+
const type = normalizeText(visual.type);
136+
const label = normalizeText(visual.label);
137+
const key = normalizeText(visual.assetKey);
138+
return `${label || key || "Assigned visual"}${type ? ` / ${type}` : ""}`;
139+
}
140+
141+
function sourceButton(object) {
142+
const button = document.createElement("button");
143+
button.className = object.key === state.selectedKey ? "btn btn--compact primary" : "btn btn--compact";
144+
button.type = "button";
145+
button.dataset.hitboxesSelectObject = object.key;
146+
button.textContent = object.name;
147+
return button;
148+
}
149+
150+
function objectRow(object) {
45151
const row = document.createElement("tr");
46-
const heading = document.createElement("th");
47-
heading.scope = "row";
48-
heading.textContent = milestone.milestone;
49-
row.append(heading, createCell(milestone.status), createCell(milestone.notes));
152+
row.dataset.hitboxesObjectRow = object.key;
153+
const nameCell = document.createElement("th");
154+
nameCell.scope = "row";
155+
nameCell.append(sourceButton(object));
156+
row.append(nameCell, tableCell(object.key), tableCell(visualLabel(object.visual)));
50157
return row;
51158
}
52159

53-
function renderFoundation() {
54-
elements.table?.replaceChildren(...FOUNDATION_MILESTONES.map(createRow));
55-
setText(elements.contractCount, String(FOUNDATION_MILESTONES.length));
56-
setText(elements.scopeCount, "0");
57-
setText(elements.foundationStatus, "Foundation ready");
58-
setText(elements.owner, "Team Delta");
59-
setText(elements.persistence, "Local API contract pending");
60-
setText(elements.service, "hitboxes-service-placeholder");
61-
setText(elements.mode, "read-only foundation");
62-
setText(elements.toolState, "Foundation");
63-
setText(elements.log, "Hitboxes foundation loaded. Drawing and editing are intentionally deferred.");
160+
function previewImageUrl(visual) {
161+
const path = normalizeText(visual.previewPath);
162+
if (!path) {
163+
return "";
164+
}
165+
if (/^(https?:|data:|blob:|\/)/i.test(path)) {
166+
return path;
167+
}
168+
return `/api/storage/project-assets/read?key=${encodeURIComponent(path)}`;
169+
}
170+
171+
function boundsText(bounds) {
172+
return `${bounds.width}x${bounds.height} at ${bounds.x},${bounds.y}`;
173+
}
174+
175+
function originText(origin) {
176+
return `${origin.x},${origin.y}`;
177+
}
178+
179+
function selectedObject() {
180+
return state.eligibleObjects.find((object) => object.key === state.selectedKey) || null;
181+
}
182+
183+
function renderList() {
184+
if (!elements.list) {
185+
return;
186+
}
187+
if (!state.eligibleObjects.length) {
188+
elements.list.replaceChildren(tableMessage(EMPTY_STATE));
189+
return;
190+
}
191+
elements.list.replaceChildren(...state.eligibleObjects.map(objectRow));
192+
}
193+
194+
function renderSelectedObject() {
195+
const object = selectedObject();
196+
setText(elements.objectCount, String(state.eligibleObjects.length));
197+
198+
if (!object) {
199+
setText(elements.selectedKey, "None");
200+
setText(elements.visualType, "None");
201+
setText(elements.previewTitle, "No Object selected");
202+
setText(elements.previewSummary, state.eligibleObjects.length ? "Select an Object with assigned visual metadata to prepare hitbox editing." : EMPTY_STATE);
203+
setText(elements.previewFallback, state.eligibleObjects.length ? "Select an Object to preview assigned visual metadata." : EMPTY_STATE);
204+
setText(elements.boundingBox, "Bounding box: none");
205+
setText(elements.originMarker, "Origin: none");
206+
setText(elements.metaName, "None");
207+
setText(elements.metaKey, "None");
208+
setText(elements.metaVisual, "None");
209+
setText(elements.metaBounds, "None");
210+
setText(elements.metaOrigin, "None");
211+
setText(elements.log, state.eligibleObjects.length ? "Select Object A." : EMPTY_STATE);
212+
if (elements.previewImage) {
213+
elements.previewImage.hidden = true;
214+
elements.previewImage.removeAttribute("src");
215+
}
216+
return;
217+
}
218+
219+
const imageUrl = previewImageUrl(object.visual);
220+
setText(elements.selectedKey, object.key);
221+
setText(elements.visualType, object.visual.type);
222+
setText(elements.previewTitle, object.name);
223+
setText(elements.previewSummary, `Object A uses ${visualLabel(object.visual)} from the Objects service contract.`);
224+
setText(elements.previewFallback, `Assigned visual metadata exists for ${object.name}; rendering is pending if no image appears.`);
225+
setText(elements.boundingBox, `Bounding box: ${boundsText(object.bounds)}`);
226+
setText(elements.originMarker, `Origin: ${originText(object.origin)}`);
227+
setText(elements.metaName, object.name);
228+
setText(elements.metaKey, object.key);
229+
setText(elements.metaVisual, visualLabel(object.visual));
230+
setText(elements.metaBounds, boundsText(object.bounds));
231+
setText(elements.metaOrigin, originText(object.origin));
232+
setText(elements.log, `Selected Object A: ${object.name}.`);
233+
234+
if (elements.previewImage) {
235+
if (imageUrl) {
236+
elements.previewImage.alt = `${object.name} assigned visual preview`;
237+
elements.previewImage.src = imageUrl;
238+
elements.previewImage.hidden = false;
239+
} else {
240+
elements.previewImage.hidden = true;
241+
elements.previewImage.removeAttribute("src");
242+
}
243+
}
244+
}
245+
246+
function render() {
247+
setText(elements.sourceStatus, state.eligibleObjects.length ? "Objects loaded from Local API." : EMPTY_STATE);
248+
renderList();
249+
renderSelectedObject();
64250
}
65251

66-
renderFoundation();
252+
function loadObjects() {
253+
const result = objectsRepository.listObjects();
254+
if (!Array.isArray(result)) {
255+
const message = result?.message || result?.validation?.findings?.[0]?.action || "Objects service is unavailable.";
256+
state.eligibleObjects = [];
257+
state.selectedKey = "";
258+
render();
259+
setText(elements.sourceStatus, message);
260+
setText(elements.log, message);
261+
return;
262+
}
263+
264+
state.eligibleObjects = result.map(eligibleObjectFromSource).filter(Boolean);
265+
const params = new URLSearchParams(window.location.search);
266+
const requestedKey = normalizeText(params.get("objectKey"));
267+
state.selectedKey = state.eligibleObjects.some((object) => object.key === requestedKey)
268+
? requestedKey
269+
: state.eligibleObjects[0]?.key || "";
270+
render();
271+
}
272+
273+
elements.list?.addEventListener("click", (event) => {
274+
const button = event.target instanceof HTMLElement ? event.target.closest("[data-hitboxes-select-object]") : null;
275+
if (!button) {
276+
return;
277+
}
278+
state.selectedKey = button.dataset.hitboxesSelectObject || "";
279+
render();
280+
});
281+
282+
elements.previewImage?.addEventListener("error", () => {
283+
const object = selectedObject();
284+
if (!object || !elements.previewImage) {
285+
return;
286+
}
287+
elements.previewImage.hidden = true;
288+
elements.previewImage.removeAttribute("src");
289+
setText(elements.previewFallback, `Assigned visual metadata exists for ${object.name}; rendering is pending for ${object.visual.previewPath || object.visual.assetKey}.`);
290+
});
291+
292+
loadObjects();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# PR_26177_DELTA_004-hitboxes-real-object-source
2+
3+
Team: Delta
4+
Branch: PR_26177_DELTA_004-hitboxes-real-object-source
5+
Base: PR_26177_DELTA_003-hitboxes-engine-collision-contract
6+
Scope: Hitboxes real Object source selection only
7+
8+
## Summary
9+
10+
Hitboxes now loads real Objects from the Objects Local API/service contract and filters the source list to Objects with assigned visual asset metadata. Selecting an eligible Object loads it as Object A and renders creator-facing preview, bounding box, origin, and metadata panels.
11+
12+
## Changed Files Summary
13+
14+
- `toolbox/hitboxes/index.html`: replaced foundation-only messaging with Object A source selection, preview, and metadata panels.
15+
- `assets/toolbox/hitboxes/js/index.js`: reads Objects via `createServerRepositoryClient("objects")`, filters eligible visual-backed Objects, and renders selection/preview state.
16+
- `tests/playwright/tools/HitboxesTool.spec.mjs`: adds targeted creator-facing Object selection coverage.
17+
- `docs_build/dev/reports/*`: required PR reports and Codex review artifacts.
18+
19+
## Scope Guard
20+
21+
- No hitbox drawing or editing added.
22+
- No Object B support added.
23+
- No browser-owned product data added.
24+
- No fake standalone Object source data added.
25+
- No unrelated tools changed.
26+
- No `start_of_day` files changed.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# PR_26177_DELTA_004 Hitboxes Real Object Source Branch Validation
2+
3+
Branch: PR_26177_DELTA_004-hitboxes-real-object-source
4+
Base: PR_26177_DELTA_003-hitboxes-engine-collision-contract
5+
6+
Result: PASS
7+
8+
| Check | Status | Notes |
9+
|---|---|---|
10+
| Current branch inside Delta Hitboxes chain | PASS | Started from `PR_26177_DELTA_003-hitboxes-engine-collision-contract`. |
11+
| Working tree clean before branch | PASS | No dirty files before branch creation. |
12+
| Stacked branch created | PASS | Work is on `PR_26177_DELTA_004-hitboxes-real-object-source`. |
13+
| No return to main | PASS | This PR was stacked from the active Delta Hitboxes branch. |
14+
| Scope is real Object source selection only | PASS | Changed Hitboxes page, Hitboxes JS, targeted Playwright test, and reports. |
15+
| No unrelated tools | PASS | No unrelated tool source files changed. |
16+
| start_of_day untouched | PASS | No `start_of_day` files changed. |
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# PR_26177_DELTA_004 Hitboxes Real Object Source Manual Validation Notes
2+
3+
- Open `toolbox/objects/index.html`.
4+
- Add or use an Object with render type `Sprite` so the Objects service assigns visual asset metadata.
5+
- Open `toolbox/hitboxes/index.html?objectKey=<object-key>&sourceTool=objects`.
6+
- Confirm the left panel lists only Objects with assigned visual metadata.
7+
- Select an Object and confirm Object A preview, bounding box, origin, and metadata update.
8+
- If the assigned asset image cannot render, confirm the fallback says metadata exists and rendering is pending.
9+
- Confirm no hitbox drawing/editing controls are present in this PR.

0 commit comments

Comments
 (0)