Skip to content

Commit 3b3d952

Browse files
author
DavidQ
committed
Use 0xProto Nerd Font glyphs for Object Vector Studio V2 icon buttons - PR_26133_018-object-vector-studio-nerd-font-icons
1 parent 1915d59 commit 3b3d952

5 files changed

Lines changed: 285 additions & 45 deletions

File tree

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# PR_26133_017 Playwright V8 Coverage Report
1+
# PR_26133_018 Playwright V8 Coverage Report
22

33
Coverage source: `docs/dev/reports/playwright_v8_coverage_report.txt`, refreshed by the final `npm run test:workspace-v2` run.
44

@@ -7,12 +7,12 @@ Coverage source: `docs/dev/reports/playwright_v8_coverage_report.txt`, refreshed
77
- Coverage is advisory only; no thresholds are enforced.
88
- Workspace Manager V2 entry point: 91%.
99
- Object Vector Studio V2 runtime coverage entries from the generated report:
10-
- `tools/object-vector-studio-v2/js/ToolStarterApp.js`: 93%, executed lines 3735/3735, executed functions 403/434.
10+
- `tools/object-vector-studio-v2/js/ToolStarterApp.js`: 93%, executed lines 3827/3827, executed functions 407/438.
1111
- The generated report lists `tests/playwright/tools/WorkspaceManagerV2.spec.mjs` as changed JS not collected by browser runtime coverage.
1212

1313
## Validation Context
1414

1515
- Main command: `npm run test:workspace-v2`.
1616
- Result: 48 passed.
1717
- Focused Object Vector Studio V2 layout, preview coordinate, geometry-layout, mouse-editing, animation-state, and asset-authoring scenarios passed as part of the workspace-v2 run.
18-
- Coverage includes the runtime paths for compact Object Details geometry rendering, selected-shape summary cleanup, selected palette swatch styling, and Triangle tool display labeling.
18+
- Coverage includes the runtime paths for the scoped Nerd Font icon mapping, static icon decoration, dynamic tile icon decoration, and existing icon button behavior checks.

docs/dev/reports/playwright_workspace_v2_results.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
# PR_26133_017 Workspace V2 Results
1+
# PR_26133_018 Workspace V2 Results
22

33
## Command Results
44

55
- `node --check tools/object-vector-studio-v2/js/ToolStarterApp.js`: passed.
66
- `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`: passed.
7+
- `npm run test:workspace-v2 -- --grep "shows Object Vector Studio V2 layout shell"`: 48 passed.
78
- `npm run test:workspace-v2`: 48 passed.
89
- `git diff --check`: passed with LF-to-CRLF working-copy warnings for touched files.
910

1011
## Targeted Object Vector Studio V2 Verification
1112

12-
- Confirmed Object Details no longer renders the `Fill Color` summary row/value.
13-
- Confirmed Rectangle, Circle, Ellipse, Line, Arc, and Text Geometry controls use compact reduced spacing.
14-
- Confirmed requested geometry input rows render on the same line: rectangle `x/y` and `width/height`, circle `cx/cy` and inline `r`, ellipse `cx/cy` and `rx/ry`, line `x1/y1` and `x2/y2`, arc `cx/cy`, and text `x/y` plus inline `fontSize` and `text` fields.
15-
- Confirmed the selected palette swatch renders with a visible selected state, outline, shadow, and check mark.
16-
- Confirmed Triangle tool creation displays `Triangle Geometry`, not `Polygon Geometry`, while preserving schema-valid polygon storage.
13+
- Confirmed Object Vector Studio V2 registers `@font-face` for `0xProto Nerd Font` from `src/shared/font/0xProtoNerdFont/0xProtoNerdFontMono-Regular.ttf`.
14+
- Confirmed static and dynamic Object Vector Studio V2 icon targets render glyphs with `0xProto Nerd Font` through the scoped icon mapping.
15+
- Confirmed object action, viewport, shape tool, z-order/group, snap/grid, transform, shape tile, and object tile icons preserve their existing click actions.
16+
- Confirmed visible button text, aria labels, and titles/tooltips remain available; disabled controls keep their existing disabled-reason tooltip behavior.
17+
- Confirmed show/hide and lock/unlock tile buttons swap to the matching visible/hidden and locked/unlocked glyph keys without changing behavior.
18+
- Confirmed `src/shared/font/0xProtoNerdFont` had no file changes.
1719
- Confirmed workspace-v2 Object Vector Studio V2 scenarios reported no console/page errors.
1820

1921
## Scope Checks

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,41 +1244,88 @@ test.describe("Workspace Manager V2 bootstrap", () => {
12441244
await expect(page.locator(".object-vector-studio-v2__tool-toggle")).toHaveText(["Select", "Triangle", "Rectangle", "Circle", "Ellipse", "Line", "Polygon", "Arc", "Text"]);
12451245
await expect(page.locator(".object-vector-studio-v2__shape-icon--triangle")).toBeVisible();
12461246
await expect(page.locator(".object-vector-studio-v2__shape-icon--arc")).toBeVisible();
1247-
const iconStyleState = await page.evaluate(() => {
1248-
const style = (selector, pseudo = null) => {
1247+
const iconStyleState = await page.evaluate(async () => {
1248+
const fontResponse = await fetch("/src/shared/font/0xProtoNerdFont/0xProtoNerdFontMono-Regular.ttf", { cache: "no-store" });
1249+
const icon = (selector) => {
12491250
const element = document.querySelector(selector);
1250-
return getComputedStyle(element, pseudo);
1251+
const before = getComputedStyle(element, "::before");
1252+
return {
1253+
fontFamily: before.fontFamily,
1254+
glyphPresent: Boolean(element.dataset.ovsIcon),
1255+
iconKey: element.dataset.ovsIconKey
1256+
};
12511257
};
1258+
const title = (selector) => document.querySelector(selector).title;
12521259
return {
1253-
arcBorderBottomColor: style(".object-vector-studio-v2__shape-icon--arc").borderBottomColor,
1254-
arcBorderBottomWidth: style(".object-vector-studio-v2__shape-icon--arc").borderBottomWidth,
1255-
circleBorderTopColor: style(".object-vector-studio-v2__shape-icon--circle").borderTopColor,
1256-
circleBorderTopWidth: style(".object-vector-studio-v2__shape-icon--circle").borderTopWidth,
1257-
ellipseBorderTopColor: style(".object-vector-studio-v2__shape-icon--ellipse").borderTopColor,
1258-
ellipseBorderTopWidth: style(".object-vector-studio-v2__shape-icon--ellipse").borderTopWidth,
1259-
polygonOutlineColor: style(".object-vector-studio-v2__shape-icon--polygon").outlineColor,
1260-
polygonOutlineWidth: style(".object-vector-studio-v2__shape-icon--polygon").outlineWidth,
1261-
rectangleBorderTopColor: style(".object-vector-studio-v2__shape-icon--rectangle").borderTopColor,
1262-
rectangleBorderTopWidth: style(".object-vector-studio-v2__shape-icon--rectangle").borderTopWidth,
1263-
triangleBackground: style(".object-vector-studio-v2__shape-icon--triangle").backgroundColor,
1264-
triangleBorderBottomWidth: style(".object-vector-studio-v2__shape-icon--triangle").borderBottomWidth,
1265-
triangleStrokeWidth: style(".object-vector-studio-v2__shape-icon--triangle", "::before").borderLeftWidth
1260+
actionIcons: {
1261+
add: icon("#objectVectorStudioV2AddObjectButton"),
1262+
delete: icon("#objectVectorStudioV2DeleteObjectButton"),
1263+
rename: icon("#objectVectorStudioV2RenameObjectButton")
1264+
},
1265+
fontAssetOk: fontResponse.ok,
1266+
shapeIcons: {
1267+
arc: icon(".object-vector-studio-v2__shape-icon--arc"),
1268+
circle: icon(".object-vector-studio-v2__shape-icon--circle"),
1269+
ellipse: icon(".object-vector-studio-v2__shape-icon--ellipse"),
1270+
polygon: icon(".object-vector-studio-v2__shape-icon--polygon"),
1271+
rectangle: icon(".object-vector-studio-v2__shape-icon--rectangle"),
1272+
select: icon(".object-vector-studio-v2__shape-icon--select"),
1273+
text: icon(".object-vector-studio-v2__shape-icon--text"),
1274+
triangle: icon(".object-vector-studio-v2__shape-icon--triangle")
1275+
},
1276+
titles: {
1277+
add: title("#objectVectorStudioV2AddObjectButton"),
1278+
grid: title("#objectVectorStudioV2GridRenderButton"),
1279+
rename: title("#objectVectorStudioV2RenameObjectButton"),
1280+
shape: title("[data-shape-tool='rectangle']"),
1281+
zoomIn: title("#objectVectorStudioV2ZoomInButton")
1282+
},
1283+
viewportIcons: {
1284+
down: icon("#objectVectorStudioV2PanDownButton"),
1285+
reset: icon("#objectVectorStudioV2ResetViewButton"),
1286+
up: icon("#objectVectorStudioV2PanUpButton"),
1287+
zoomIn: icon("#objectVectorStudioV2ZoomInButton"),
1288+
zoomOut: icon("#objectVectorStudioV2ZoomOutButton")
1289+
},
1290+
zIcons: {
1291+
group: icon(".object-vector-studio-v2__z-icon--group"),
1292+
ungroup: icon(".object-vector-studio-v2__z-icon--ungroup")
1293+
}
12661294
};
12671295
});
1268-
expect(iconStyleState).toEqual({
1269-
arcBorderBottomColor: "rgb(255, 255, 255)",
1270-
arcBorderBottomWidth: "4px",
1271-
circleBorderTopColor: "rgb(255, 255, 255)",
1272-
circleBorderTopWidth: "4px",
1273-
ellipseBorderTopColor: "rgb(255, 255, 255)",
1274-
ellipseBorderTopWidth: "4px",
1275-
polygonOutlineColor: "rgb(255, 255, 255)",
1276-
polygonOutlineWidth: "4px",
1277-
rectangleBorderTopColor: "rgb(255, 255, 255)",
1278-
rectangleBorderTopWidth: "4px",
1279-
triangleBackground: "rgba(0, 0, 0, 0)",
1280-
triangleBorderBottomWidth: "0px",
1281-
triangleStrokeWidth: "4px"
1296+
expect(iconStyleState.fontAssetOk).toBe(true);
1297+
[
1298+
...Object.values(iconStyleState.actionIcons),
1299+
...Object.values(iconStyleState.shapeIcons),
1300+
...Object.values(iconStyleState.viewportIcons),
1301+
...Object.values(iconStyleState.zIcons)
1302+
].forEach((icon) => {
1303+
expect(icon.glyphPresent).toBe(true);
1304+
expect(icon.fontFamily).toContain("0xProto Nerd Font");
1305+
});
1306+
expect(Object.fromEntries(Object.entries(iconStyleState.shapeIcons).map(([key, value]) => [key, value.iconKey]))).toEqual({
1307+
arc: "arc",
1308+
circle: "circle",
1309+
ellipse: "ellipse",
1310+
polygon: "polygon",
1311+
rectangle: "rectangle",
1312+
select: "select",
1313+
text: "text",
1314+
triangle: "triangle"
1315+
});
1316+
expect(Object.fromEntries(Object.entries(iconStyleState.viewportIcons).map(([key, value]) => [key, value.iconKey]))).toEqual({
1317+
down: "panDown",
1318+
reset: "reset",
1319+
up: "panUp",
1320+
zoomIn: "zoomIn",
1321+
zoomOut: "zoomOut"
1322+
});
1323+
expect(iconStyleState.titles).toEqual({
1324+
add: "Add a schema-valid object to the loaded payload",
1325+
grid: "Show or hide the preview grid",
1326+
rename: "Disabled until a schema-valid object is selected.",
1327+
shape: "Create a rectangle shape on the selected object",
1328+
zoomIn: "Zoom the work surface in"
12821329
});
12831330

12841331
await page.locator('[data-shape-tool="rectangle"]').click();
@@ -1714,6 +1761,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17141761
controlOrder: controls.map((control) => control.dataset.objectControl),
17151762
deleteAtFarRight: Math.abs(tileRect.right - deleteRect.right) <= 12,
17161763
deleteTitle: deleteButton.title,
1764+
iconFontsUseNerd: controls.every((control) => getComputedStyle(control.querySelector("[data-ovs-icon]"), "::before").fontFamily.includes("0xProto Nerd Font")),
1765+
iconKeys: controls.map((control) => control.querySelector("[data-ovs-icon]")?.dataset.ovsIconKey),
17171766
iconSizes: rects.map((rect) => Math.round(Math.max(rect.width, rect.height))),
17181767
stacked: rects.every((rect, index) => index === 0
17191768
|| (Math.abs(rect.left - rects[index - 1].left) <= 1 && rect.top > rects[index - 1].top))
@@ -1724,6 +1773,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17241773
controlOrder: ["visibility", "lock", "delete"],
17251774
deleteAtFarRight: true,
17261775
deleteTitle: "Delete this object",
1776+
iconFontsUseNerd: true,
1777+
iconKeys: ["eye", "unlock", "delete"],
17271778
iconSizes: [26, 26, 26],
17281779
stacked: true
17291780
});
@@ -1735,11 +1786,14 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17351786
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-control='delete'] .object-vector-studio-v2__tile-icon--delete")).toHaveCount(1);
17361787
await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-control='visibility']").click();
17371788
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1']")).toHaveClass(/is-hidden/);
1789+
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-control='visibility'] [data-ovs-icon-key='eyeOff']")).toHaveCount(1);
17381790
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-id]")).toHaveCount(0);
17391791
await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-control='visibility']").click();
1792+
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-control='visibility'] [data-ovs-icon-key='eye']")).toHaveCount(1);
17401793
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-id]")).toHaveCount(2);
17411794
await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-control='lock']").click();
17421795
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1']")).toHaveClass(/is-locked/);
1796+
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-control='lock'] [data-ovs-icon-key='lock']")).toHaveCount(1);
17431797
await expect(page.locator("#objectVectorStudioV2RenameObjectButton")).toBeDisabled();
17441798
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-control='delete']")).toBeDisabled();
17451799
await page.locator('[data-shape-tool="line"]').click();

tools/object-vector-studio-v2/js/ToolStarterApp.js

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,81 @@ const MAX_ZOOM = 0.5;
1616
const MIN_ZOOM = 0.01;
1717
const ZOOM_STEP = 0.01;
1818

19+
const OBJECT_VECTOR_STUDIO_ICON_GLYPHS = Object.freeze({
20+
add: "\uf067",
21+
angle: "\uf1de",
22+
arc: "\uf1da",
23+
bringForward: "\uf062",
24+
bringFront: "\uf102",
25+
center: "\uf192",
26+
circle: "\uf111",
27+
delete: "\uf00d",
28+
duplicate: "\uf0c5",
29+
edit: "\uf044",
30+
ellipse: "\uf111",
31+
eye: "\uf06e",
32+
eyeOff: "\uf070",
33+
grid: "\uf00a",
34+
group: "\uf247",
35+
line: "\uf068",
36+
lock: "\uf023",
37+
move: "\uf047",
38+
panDown: "\uf063",
39+
panLeft: "\uf060",
40+
panRight: "\uf061",
41+
panUp: "\uf062",
42+
polygon: "\uf1fe",
43+
rectangle: "\uf096",
44+
reset: "\uf0e2",
45+
resize: "\uf065",
46+
rotate: "\uf01e",
47+
scale: "\uf065",
48+
select: "\uf245",
49+
sendBack: "\uf103",
50+
sendBackward: "\uf063",
51+
text: "\uf031",
52+
triangle: "\uf0d8",
53+
ungroup: "\uf248",
54+
unlock: "\uf09c",
55+
zoomIn: "\uf00e",
56+
zoomOut: "\uf010"
57+
});
58+
59+
const OBJECT_VECTOR_STUDIO_STATIC_ICON_TARGETS = Object.freeze([
60+
["#objectVectorStudioV2AddTagButton", "add"],
61+
["#objectVectorStudioV2AddObjectButton", "add"],
62+
["#objectVectorStudioV2RenameObjectButton", "edit"],
63+
["#objectVectorStudioV2DuplicateObjectButton", "duplicate"],
64+
["#objectVectorStudioV2DeleteObjectButton", "delete"],
65+
["#objectVectorStudioV2ZoomOutButton", "zoomOut"],
66+
["#objectVectorStudioV2ZoomInButton", "zoomIn"],
67+
["#objectVectorStudioV2PanUpButton", "panUp"],
68+
["#objectVectorStudioV2PanDownButton", "panDown"],
69+
["#objectVectorStudioV2PanLeftButton", "panLeft"],
70+
["#objectVectorStudioV2PanRightButton", "panRight"],
71+
["#objectVectorStudioV2ResetViewButton", "reset"],
72+
["#objectVectorStudioV2CenterDotButton", "center"],
73+
["#objectVectorStudioV2DuplicateFrameButton", "duplicate"],
74+
["#objectVectorStudioV2GridSnapButton", "grid"],
75+
["#objectVectorStudioV2AngleSnapButton", "angle"],
76+
["#objectVectorStudioV2GridRenderButton", "grid"],
77+
[".object-vector-studio-v2__shape-icon--select", "select"],
78+
[".object-vector-studio-v2__shape-icon--triangle", "triangle"],
79+
[".object-vector-studio-v2__shape-icon--rectangle", "rectangle"],
80+
[".object-vector-studio-v2__shape-icon--circle", "circle"],
81+
[".object-vector-studio-v2__shape-icon--ellipse", "ellipse"],
82+
[".object-vector-studio-v2__shape-icon--line", "line"],
83+
[".object-vector-studio-v2__shape-icon--polygon", "polygon"],
84+
[".object-vector-studio-v2__shape-icon--arc", "arc"],
85+
[".object-vector-studio-v2__shape-icon--text", "text"],
86+
[".object-vector-studio-v2__z-icon--bring-forward", "bringForward"],
87+
[".object-vector-studio-v2__z-icon--send-backward", "sendBackward"],
88+
[".object-vector-studio-v2__z-icon--bring-front", "bringFront"],
89+
[".object-vector-studio-v2__z-icon--send-back", "sendBack"],
90+
[".object-vector-studio-v2__z-icon--group", "group"],
91+
[".object-vector-studio-v2__z-icon--ungroup", "ungroup"]
92+
]);
93+
1994
const OBJECT_STATE_IDS = Object.freeze(["idle", "thrust", "damaged", "destroyed", "active", "inactive"]);
2095

2196
const OBJECT_STATE_LABELS = Object.freeze({
@@ -263,6 +338,7 @@ export class ToolStarterApp {
263338
}
264339
});
265340
this.statusLog.mount();
341+
this.applyNerdFontIcons();
266342
this.bindObjectActions();
267343
this.bindToolToggles();
268344
this.bindSnapControls();
@@ -404,6 +480,22 @@ export class ToolStarterApp {
404480
this.elements.ungroupButton.addEventListener("click", () => this.ungroupSelectedShapes());
405481
}
406482

483+
applyNerdFontIcons() {
484+
OBJECT_VECTOR_STUDIO_STATIC_ICON_TARGETS.forEach(([selector, iconKey]) => {
485+
this.window.document.querySelectorAll(selector).forEach((element) => this.applyIconGlyph(element, iconKey));
486+
});
487+
}
488+
489+
applyIconGlyph(element, iconKey) {
490+
const glyph = OBJECT_VECTOR_STUDIO_ICON_GLYPHS[iconKey];
491+
if (!element || !glyph) {
492+
return;
493+
}
494+
element.dataset.ovsIcon = glyph;
495+
element.dataset.ovsIconKey = iconKey;
496+
element.classList.add("object-vector-studio-v2__nerd-icon");
497+
}
498+
407499
bindAnimationControls() {
408500
this.elements.duplicateFrameButton.addEventListener("click", () => this.duplicateSelectedFrame());
409501
this.elements.frameEarlierButton.addEventListener("click", () => this.moveSelectedFrame("earlier"));
@@ -1019,6 +1111,8 @@ export class ToolStarterApp {
10191111
icon.className = `object-vector-studio-v2__tile-icon object-vector-studio-v2__tile-icon--${kind}`;
10201112
icon.classList.toggle("is-off", !isActive);
10211113
icon.setAttribute("aria-hidden", "true");
1114+
const iconKey = kind === "eye" && !isActive ? "eyeOff" : kind === "lock" && !isActive ? "unlock" : kind;
1115+
this.applyIconGlyph(icon, iconKey);
10221116
return icon;
10231117
}
10241118

@@ -1474,6 +1568,7 @@ export class ToolStarterApp {
14741568
applyButton.id = "objectVectorStudioV2ApplyGeometryButton";
14751569
applyButton.type = "button";
14761570
applyButton.textContent = "Apply Geometry";
1571+
this.applyIconGlyph(applyButton, "edit");
14771572
applyButton.addEventListener("click", () => this.applyShapeGeometryEdits());
14781573
section.append(heading, grid, applyButton);
14791574
return section;
@@ -1515,16 +1610,17 @@ export class ToolStarterApp {
15151610
const actions = document.createElement("div");
15161611
actions.className = "object-vector-studio-v2__shape-actions";
15171612
[
1518-
["objectVectorStudioV2MoveShapeButton", "Move", () => this.moveSelectedShape()],
1519-
["objectVectorStudioV2RotateShapeButton", "Rotate", () => this.rotateSelectedShape()],
1520-
["objectVectorStudioV2ScaleShapeButton", "Scale", () => this.scaleSelectedShape()],
1521-
["objectVectorStudioV2ResizeShapeButton", "Resize", () => this.resizeSelectedShape()],
1522-
["objectVectorStudioV2ApplyOriginButton", "Apply Origin", () => this.applySelectedShapeOrigin()]
1523-
].forEach(([id, label, handler]) => {
1613+
["objectVectorStudioV2MoveShapeButton", "Move", "move", () => this.moveSelectedShape()],
1614+
["objectVectorStudioV2RotateShapeButton", "Rotate", "rotate", () => this.rotateSelectedShape()],
1615+
["objectVectorStudioV2ScaleShapeButton", "Scale", "scale", () => this.scaleSelectedShape()],
1616+
["objectVectorStudioV2ResizeShapeButton", "Resize", "resize", () => this.resizeSelectedShape()],
1617+
["objectVectorStudioV2ApplyOriginButton", "Apply Origin", "center", () => this.applySelectedShapeOrigin()]
1618+
].forEach(([id, label, iconKey, handler]) => {
15241619
const button = document.createElement("button");
15251620
button.id = id;
15261621
button.type = "button";
15271622
button.textContent = label;
1623+
this.applyIconGlyph(button, iconKey);
15281624
button.addEventListener("click", handler);
15291625
actions.append(button);
15301626
});

0 commit comments

Comments
 (0)