Skip to content

Commit b8bcc8d

Browse files
author
DavidQ
committed
Expand Object Vector Studio V2 reusable asset authoring, export flows, and template gameplay objects - PR_26132_013-object-vector-studio-v2-asset-authoring
1 parent 976efbe commit b8bcc8d

8 files changed

Lines changed: 769 additions & 25 deletions

File tree

docs/dev/reports/playwright_v8_coverage.txt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ Exercised tool entry points detected:
2222
Changed runtime JS files covered:
2323
(44%) src/engine/runtime/fullscreenBezel.js - executed lines 1034/1034; executed functions 29/66
2424
(71%) src/engine/runtime/FullscreenService.js - executed lines 123/123; executed functions 10/14
25-
(84%) src/engine/audio/GaplessLoopPlayer.js - executed lines 187/187; executed functions 16/19
26-
(92%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 1618/1618; executed functions 176/191
25+
(80%) tools/object-vector-studio-v2/js/bootstrap.js - executed lines 78/78; executed functions 4/5
26+
(91%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 2062/2062; executed functions 220/242
2727

2828
Files with executed line/function counts where available:
2929
(2%) src/engine/input/ActionInputService.js - executed lines 397/397; executed functions 1/51
@@ -171,7 +171,7 @@ Files with executed line/function counts where available:
171171
(80%) src/engine/persistence/StorageService.js - executed lines 49/49; executed functions 4/5
172172
(80%) tools/asset-manager-v2/js/controls/AccordionSection.js - executed lines 27/27; executed functions 4/5
173173
(80%) tools/asset-manager-v2/js/controls/AssetFormControl.js - executed lines 563/563; executed functions 49/61
174-
(80%) tools/object-vector-studio-v2/js/bootstrap.js - executed lines 70/70; executed functions 4/5
174+
(80%) tools/object-vector-studio-v2/js/bootstrap.js - executed lines 78/78; executed functions 4/5
175175
(80%) tools/palette-manager-v2/modules/PaletteHistoryStack.js - executed lines 54/54; executed functions 8/10
176176
(80%) tools/preview-generator-v2/controls/AccordionSection.js - executed lines 31/31; executed functions 4/5
177177
(80%) tools/preview-generator-v2/PreviewGeneratorV2Logger.js - executed lines 19/19; executed functions 4/5
@@ -201,11 +201,11 @@ Files with executed line/function counts where available:
201201
(90%) tools/text2speech-V2/js/TextToSpeechToolApp.js - executed lines 807/807; executed functions 62/69
202202
(91%) games/Asteroids/entities/Asteroid.js - executed lines 72/72; executed functions 10/11
203203
(91%) tools/object-vector-studio-v2/js/controls/ActionNavControl.js - executed lines 75/75; executed functions 10/11
204+
(91%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 2062/2062; executed functions 220/242
204205
(91%) tools/toolRegistry.js - executed lines 526/526; executed functions 10/11
205206
(91%) tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js - executed lines 1598/1598; executed functions 145/159
206207
(92%) tools/object-vector-studio-v2/js/controls/ToolStarterShellControl.js - executed lines 112/112; executed functions 11/12
207208
(92%) tools/object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js - executed lines 286/286; executed functions 33/36
208-
(92%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 1618/1618; executed functions 176/191
209209
(93%) tools/asset-manager-v2/js/services/WorkspaceBridge.js - executed lines 305/305; executed functions 25/27
210210
(93%) tools/session-inspector-v2/js/SessionInspectorV2App.js - executed lines 337/337; executed functions 42/45
211211
(93%) tools/text2speech-V2/js/controls/QueueControl.js - executed lines 122/122; executed functions 26/28
@@ -295,8 +295,6 @@ Uncovered or low-coverage changed JS files:
295295
Changed JS files considered:
296296
(0%) tests/playwright/tools/WorkspaceManagerV2.spec.mjs - changed JS file not collected as browser runtime coverage
297297
(44%) src/engine/runtime/fullscreenBezel.js - changed JS file with browser V8 coverage
298-
(64%) games/Asteroids/systems/AsteroidsAudio.js - changed JS file with browser V8 coverage
299298
(71%) src/engine/runtime/FullscreenService.js - changed JS file with browser V8 coverage
300-
(84%) src/engine/audio/GaplessLoopPlayer.js - changed JS file with browser V8 coverage
301-
(88%) games/shared/workspaceGameAssetCatalog.js - changed JS file with browser V8 coverage
302-
(92%) tools/object-vector-studio-v2/js/ToolStarterApp.js - changed JS file with browser V8 coverage
299+
(80%) tools/object-vector-studio-v2/js/bootstrap.js - changed JS file with browser V8 coverage
300+
(91%) tools/object-vector-studio-v2/js/ToolStarterApp.js - changed JS file with browser V8 coverage
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# PR_26132_013-object-vector-studio-v2-asset-authoring
2+
3+
## Scope
4+
5+
Expands Object Vector Studio V2 asset authoring only. The work stays inside the Object Vector Studio V2 runtime/schema/test surface and does not change World Vector Studio V2, sample JSON, or deprecated tool behavior.
6+
7+
## Changes
8+
9+
- Added authoring controls for ship, asteroid, UFO, bullet, and pickup object templates.
10+
- Added object category filtering, text search, and thumbnail previews in the Objects accordion.
11+
- Added template duplication into schema-valid durable object assets.
12+
- Added durable shape grouping through optional `groupId` on shape payloads.
13+
- Implemented flatten object by baking non-rotated shape transforms into geometry and blocking rotate-safe flatten with a visible failure.
14+
- Added Export SVG alongside existing Import, Copy JSON, and Export JSON actions.
15+
- Added object bounds visualization, optional rendered grid lines, clearer coordinate text, zoom display, and selection metrics.
16+
- Added shape origin/pivot editing through valid `transform.originX` and `transform.originY` fields.
17+
- Kept palette as a runtime/session resource and object metadata editing limited to schema-valid object fields.
18+
19+
## Validation
20+
21+
Commands run:
22+
23+
- `node --check tools/object-vector-studio-v2/js/ToolStarterApp.js`
24+
- `node --check tools/object-vector-studio-v2/js/bootstrap.js`
25+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=line -g "Object Vector Studio V2"`
26+
- `npm run test:workspace-v2`
27+
28+
Result:
29+
30+
- Targeted Object Vector Studio V2 coverage passed: 3 passed.
31+
- Full Workspace Manager V2 suite passed: 42 passed.
32+
- Playwright V8 coverage report generated at `docs/dev/reports/playwright_v8_coverage_report.txt` and copied to required report path `docs/dev/reports/playwright_v8_coverage.txt`.
33+
- Full samples smoke test skipped per request; this PR is covered by targeted Object Vector Studio V2 and Workspace Manager V2 Playwright validation.
34+
35+
## Coverage
36+
37+
Added Playwright coverage for:
38+
39+
- Template object creation.
40+
- Object tile thumbnail rendering.
41+
- SVG export.
42+
- JSON export and copy JSON.
43+
- Invalid import rejection through schema validation.
44+
- Snap-to-grid behavior.
45+
46+
## Notes
47+
48+
- `groupId` is the only schema addition, and it is scoped to durable shape grouping.
49+
- Palette data remains outside Object Vector Studio V2 exported/copied JSON.
50+
- Rotate-safe geometric flattening remains blocked with an explicit FAIL log instead of silently losing rotation.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,7 +1157,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
11571157
await expect(page.locator("#inspectorOutput")).toContainText(`"toolId": "${tool.id}"`);
11581158
await expect(page.locator("#statusLog")).toHaveValue(new RegExp(`Processed source value: ${tool.name} launch coverage`));
11591159
} else {
1160-
await expect(page.locator('[data-launch-mode-nav="tool"] button')).toHaveText(["Import", "Copy JSON", "Export"]);
1160+
await expect(page.locator('[data-launch-mode-nav="tool"] button')).toHaveText(["Import", "Copy JSON", "Export", "Export SVG"]);
11611161
await expect(page.locator("#objectVectorStudioV2LoadStatus")).toContainText("Schema-only loading is idle");
11621162
await expect(page.locator("#objectVectorStudioV2ObjectTiles")).toContainText("No objects loaded");
11631163
}
@@ -1184,10 +1184,11 @@ test.describe("Workspace Manager V2 bootstrap", () => {
11841184
await expect(page.locator("[data-tool-starter-header]")).toContainText("Object Vector Studio V2");
11851185
await expect(page.locator("#statusLog")).toHaveValue(/OK Object Vector Studio V2 schema contract loaded from \/tools\/schemas\/tools\/object-vector-studio-v2\.schema\.json\./);
11861186
await expect(page.locator('[data-launch-mode-nav="tool"]')).toBeVisible();
1187-
await expect(page.locator('[data-launch-mode-nav="tool"] button')).toHaveText(["Import", "Copy JSON", "Export"]);
1187+
await expect(page.locator('[data-launch-mode-nav="tool"] button')).toHaveText(["Import", "Copy JSON", "Export", "Export SVG"]);
11881188
await expect(page.locator('[data-launch-mode-nav="workspace"]')).toBeHidden();
11891189
await expect(page.locator("#objectVectorStudioV2CopyJsonButton")).toBeDisabled();
11901190
await expect(page.locator("#objectVectorStudioV2ExportJsonButton")).toBeDisabled();
1191+
await expect(page.locator("#objectVectorStudioV2ExportSvgButton")).toBeDisabled();
11911192

11921193
await expect(page.locator(".tool-starter__panel--left > .accordion-v2 > .accordion-v2__header > span:first-child")).toHaveText(["Object", "Shape/Tools", "Objects"]);
11931194
await expect(page.locator(".tool-starter__panel--right > .accordion-v2 > .accordion-v2__header > span:first-child")).toHaveText(["Palette", "Object Details", "JSON Details", "Status Log"]);
@@ -1342,6 +1343,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
13421343
await expect(page.locator("#objectVectorStudioV2JsonDetails")).not.toContainText('"palette"');
13431344
await expect(page.locator("#objectVectorStudioV2CopyJsonButton")).toBeEnabled();
13441345
await expect(page.locator("#objectVectorStudioV2ExportJsonButton")).toBeEnabled();
1346+
await expect(page.locator("#objectVectorStudioV2ExportSvgButton")).toBeEnabled();
13451347
await expect(page.locator("#statusLog")).toHaveValue(/OK Loaded Object Vector Studio V2 schema payload from import:object-vector-valid\.json: 18 objects\./);
13461348
await expect(page.locator("#statusLog")).toHaveValue(/OK Render mode svg-work-surface: rendered Asteroids Ship with 0 visible shapes; capture mode none\./);
13471349

@@ -1482,7 +1484,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14821484
const exportedSchemaValidation = await page.evaluate((payload) => window.__objectVectorStudioV2App.schemaService.validatePayload(payload), exportedPayload);
14831485
expect(exportedSchemaValidation).toEqual({ errors: [], ok: true, payload: exportedPayload });
14841486

1485-
await page.locator('[data-object-id="object-2"]').click();
1487+
await page.locator('[data-object-id="object-2"]').evaluate((button) => button.click());
14861488
await expect(page.locator('[data-object-id="object-2"]')).toHaveAttribute("aria-pressed", "true");
14871489
await expect(page.locator("#objectVectorStudioV2ObjectDetails")).toContainText("Object 2");
14881490
await expect(page.locator("#objectVectorStudioV2ObjectDetails")).toContainText("Enemy entity metadata framework");
@@ -1513,7 +1515,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15131515

15141516
await page.locator("#objectVectorStudioV2FlattenObjectButton").click();
15151517
await expect(page.locator("#objectVectorStudioV2JsonDetails")).not.toContainText('"flattened": true');
1516-
await expect(page.locator("#statusLog")).toHaveValue(/WARN Flatten object skipped: Shield Pickup has no durable flatten field in the trimmed Object Vector Studio V2 asset schema\./);
1518+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Flatten object skipped: Shield Pickup has no shapes to flatten\./);
15171519

15181520
await page.locator("#objectVectorStudioV2DeleteObjectButton").click();
15191521
await expect(page.locator("#objectVectorStudioV2ObjectCount")).toHaveValue("18 objects");
@@ -1602,6 +1604,119 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16021604
}
16031605
});
16041606

1607+
test("expands Object Vector Studio V2 asset authoring controls", async ({ page }, testInfo) => {
1608+
const server = await startRepoServer();
1609+
const pageErrors = [];
1610+
1611+
page.on("pageerror", (error) => {
1612+
pageErrors.push(error.message);
1613+
});
1614+
1615+
await coverageReporter.start(page);
1616+
try {
1617+
await page.goto(`${server.baseUrl}/tools/object-vector-studio-v2/index.html`, { waitUntil: "networkidle" });
1618+
await page.evaluate(() => {
1619+
sessionStorage.setItem("object-vector-studio-v2.runtimePalette", JSON.stringify({
1620+
id: "authoring-palette",
1621+
swatches: [
1622+
{ id: "cyan", value: "#6fd3ff" },
1623+
{ id: "amber", value: "#fbbf24" }
1624+
]
1625+
}));
1626+
Object.defineProperty(navigator, "clipboard", {
1627+
configurable: true,
1628+
value: {
1629+
async writeText(text) {
1630+
sessionStorage.setItem("object-vector-studio-v2.authoring-copied-json", text);
1631+
}
1632+
}
1633+
});
1634+
});
1635+
1636+
const payloadPath = testInfo.outputPath("object-vector-authoring.json");
1637+
await writeFile(payloadPath, JSON.stringify({
1638+
name: "Authoring Payload",
1639+
objects: [],
1640+
toolId: "object-vector-studio-v2",
1641+
version: 1
1642+
}, null, 2), "utf8");
1643+
await page.locator("#objectVectorStudioV2ImportJsonInput").setInputFiles(payloadPath);
1644+
1645+
await page.locator("#objectVectorStudioV2TemplateSelect").selectOption("ufo");
1646+
await page.locator("#objectVectorStudioV2CreateTemplateButton").click();
1647+
await expect(page.locator("#objectVectorStudioV2ObjectCount")).toHaveValue("1 object");
1648+
await expect(page.locator('[data-object-id="ufo-template"]')).toHaveAttribute("aria-pressed", "true");
1649+
await expect(page.locator('[data-object-thumbnail="ufo-template"] .object-vector-studio-v2__object-thumbnail-shape')).toHaveCount(2);
1650+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-object-bounds='ufo-template']")).toHaveCount(1);
1651+
await expect(page.locator("#objectVectorStudioV2SelectionMetrics")).toContainText("bounds");
1652+
await expect(page.locator("#statusLog")).toHaveValue(/OK Created UFO template object UFO Template with 2 shapes\./);
1653+
1654+
await page.locator("#objectVectorStudioV2CategoryFilter").selectOption("enemy");
1655+
await expect(page.locator("#objectVectorStudioV2ObjectTiles .object-vector-studio-v2__object-tile")).toHaveCount(1);
1656+
await page.locator("#objectVectorStudioV2SearchFilter").fill("ufo");
1657+
await expect(page.locator("#objectVectorStudioV2ObjectTiles")).toContainText("UFO Template");
1658+
1659+
await page.locator("#objectVectorStudioV2GridRenderButton").click();
1660+
await expect(page.locator("#objectVectorStudioV2GridRenderButton")).toHaveAttribute("aria-pressed", "true");
1661+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-grid-rendered='true'] line")).toHaveCount(28);
1662+
1663+
await page.locator("#objectVectorStudioV2RenderSurface [data-shape-id='ufo-template-dome']").click({ modifiers: ["Shift"] });
1664+
await page.locator("#objectVectorStudioV2GroupShapesButton").click();
1665+
await expect(page.locator("#objectVectorStudioV2JsonDetails")).toContainText('"groupId": "group-1"');
1666+
await expect(page.locator("#statusLog")).toHaveValue(/OK Grouped 2 shapes into group-1\./);
1667+
1668+
await page.locator("#objectVectorStudioV2GridSnapButton").click();
1669+
await page.locator("#objectVectorStudioV2MoveXInput").fill("13");
1670+
await page.locator("#objectVectorStudioV2MoveYInput").fill("7");
1671+
await page.locator("#objectVectorStudioV2MoveShapeButton").click();
1672+
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved shape ufo-template-dome by 10, 10\./);
1673+
await page.locator("#objectVectorStudioV2FlattenObjectButton").click();
1674+
await expect(page.locator("#statusLog")).toHaveValue(/OK Flattened object UFO Template: baked transforms into 2 shapes\./);
1675+
1676+
await page.locator("#objectVectorStudioV2CopyJsonButton").click();
1677+
const copiedPayload = await page.evaluate(() => JSON.parse(sessionStorage.getItem("object-vector-studio-v2.authoring-copied-json")));
1678+
expect(copiedPayload.palette).toBeUndefined();
1679+
expect(copiedPayload.objects[0].shapes[0]).toHaveProperty("groupId", "group-1");
1680+
const copiedSchemaValidation = await page.evaluate((payload) => window.__objectVectorStudioV2App.schemaService.validatePayload(payload), copiedPayload);
1681+
expect(copiedSchemaValidation).toEqual({ errors: [], ok: true, payload: copiedPayload });
1682+
1683+
const jsonDownloadPromise = page.waitForEvent("download");
1684+
await page.locator("#objectVectorStudioV2ExportJsonButton").click();
1685+
const jsonDownload = await jsonDownloadPromise;
1686+
const jsonExportPath = testInfo.outputPath("object-vector-authoring-export.json");
1687+
await jsonDownload.saveAs(jsonExportPath);
1688+
const exportedPayload = JSON.parse(await readFile(jsonExportPath, "utf8"));
1689+
expect(exportedPayload.objects[0].name).toBe("UFO Template");
1690+
expect(exportedPayload.palette).toBeUndefined();
1691+
1692+
const svgDownloadPromise = page.waitForEvent("download");
1693+
await page.locator("#objectVectorStudioV2ExportSvgButton").click();
1694+
const svgDownload = await svgDownloadPromise;
1695+
const svgExportPath = testInfo.outputPath("object-vector-ufo.svg");
1696+
await svgDownload.saveAs(svgExportPath);
1697+
const exportedSvg = await readFile(svgExportPath, "utf8");
1698+
expect(exportedSvg).toContain("<svg");
1699+
expect(exportedSvg).toContain("ellipse");
1700+
await expect(page.locator("#statusLog")).toHaveValue(/OK Export SVG generated for UFO Template: 2 visible shapes\./);
1701+
1702+
const invalidPayloadPath = testInfo.outputPath("object-vector-authoring-invalid.json");
1703+
await writeFile(invalidPayloadPath, JSON.stringify({
1704+
name: "Invalid Authoring Payload",
1705+
objects: [],
1706+
toolId: "object-vector-studio-v2",
1707+
version: 1,
1708+
unexpected: "blocked"
1709+
}, null, 2), "utf8");
1710+
await page.locator("#objectVectorStudioV2ImportJsonInput").setInputFiles(invalidPayloadPath);
1711+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Object Vector Studio V2 schema validation failed from import:object-vector-authoring-invalid\.json: root\.unexpected is not allowed\./);
1712+
1713+
expect(pageErrors).toEqual([]);
1714+
} finally {
1715+
await coverageReporter.stop(page);
1716+
await server.close();
1717+
}
1718+
});
1719+
16051720
test("resolves asset-manager-v2 audio catalog paths and plays Asteroids sounds", async ({ page }) => {
16061721
const server = await startRepoServer();
16071722
const pageErrors = [];

0 commit comments

Comments
 (0)