Skip to content

Commit d10ec78

Browse files
author
DavidQ
committed
Add selected geometry point handles and clean shape tile group icon layout - PR_26133_055-shape-tile-group-icon-and-geometry-point-handles
1 parent 48849fc commit d10ec78

5 files changed

Lines changed: 428 additions & 51 deletions

File tree

docs/dev/reports/playwright_v8_coverage_report.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_054 Playwright V8 Coverage Report
1+
# PR_26133_055 Playwright V8 Coverage Report
22

3-
Task: PR_26133_054-group-regroup-group-move-and-state-delete-restore
3+
Task: PR_26133_055-shape-tile-group-icon-and-geometry-point-handles
44
Date: 2026-05-15
55

66
## Result
@@ -24,7 +24,7 @@ PASS - Coverage reporting was generated during `npm run test:workspace-v2`.
2424
## Relevant Runtime Coverage
2525

2626
```text
27-
(95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 5213/5213; executed functions 564/591
27+
(95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 5496/5496; executed functions 576/606
2828
```
2929

3030
## Guardrail
@@ -35,4 +35,4 @@ PASS - Coverage reporting was generated during `npm run test:workspace-v2`.
3535

3636
## PR-Specific Note
3737

38-
The Workspace V2 run exercised Object Vector Studio V2 regroup behavior, selected-group movement, single-shape movement, state add/delete controls, final-state delete prevention, dirty-state tracking, schema validation, and Asteroids runtime object-vector rendering.
38+
The Workspace V2 run exercised Object Vector Studio V2 shape tile group indicator layout, preview geometry point handles, line endpoint handles, polygon point drag editing, bounding-box geometry resize, dirty-state tracking, schema validation, and Asteroids runtime object-vector rendering.
Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_054 Workspace V2 Playwright Results
1+
# PR_26133_055 Workspace V2 Playwright Results
22

3-
Task: PR_26133_054-group-regroup-group-move-and-state-delete-restore
3+
Task: PR_26133_055-shape-tile-group-icon-and-geometry-point-handles
44
Date: 2026-05-15
55

66
## Result
@@ -14,20 +14,19 @@ PASS - `npm run test:workspace-v2` completed successfully.
1414

1515
## PR-Specific Coverage
1616

17-
- Verified regrouping selected shapes moves only the directly selected shapes into the new group.
18-
- Verified old groups are pruned when fewer than two shapes remain and orphan group indicators disappear.
19-
- Verified unselected members from an old group do not move into the new group.
20-
- Verified Move moves all shapes in the selected group while preserving relative positions.
21-
- Verified Move affects only one shape when the selected shape is not grouped.
22-
- Verified Delete State renders, deletes only the selected state, refreshes state tiles/timeline immediately, and blocks deleting the final remaining state.
23-
- Verified Delete State marks Object Vector Studio V2 workspace state dirty after a successful persisted edit.
17+
- Verified grouped shape tile indicators render at the far right of shape rows while shape index/tool text stays on the left.
18+
- Verified rectangle corner handles expose geometry-point metadata and still resize geometry through bounding-box corner drag.
19+
- Verified line endpoint handles expose geometry-point metadata and still update point1/point2 geometry.
20+
- Verified polygon point handles render on each selected point and dragging a point updates the underlying geometry plus Object Geometry inputs.
21+
- Verified polygon bounding-box corner dragging adjusts geometry points.
22+
- Verified geometry handle drags mark Object Vector Studio V2 workspace state dirty after persisted edits.
2423

2524
## Additional Validation
2625

27-
- Focused regroup/state slice passed:
28-
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "single-member groups"` completed with 1 passed, 0 failed.
29-
- Focused grouped authoring slice passed:
30-
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "asset authoring controls"` completed with 1 passed, 0 failed.
26+
- Focused preview-handle slice passed:
27+
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "preview shapes with mouse actions"` completed with 1 passed, 0 failed.
28+
- Focused group tile layout slice passed:
29+
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "layout shell"` completed with 1 passed, 0 failed.
3130
- Focused dirty-state slice passed:
3231
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "dirty state through persisted edits"` completed with 1 passed, 0 failed.
3332
- `git diff --check` passed.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2528,6 +2528,27 @@ test.describe("Workspace Manager V2 bootstrap", () => {
25282528
const groupIconColors = await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-shape-group-id='group-1']").evaluateAll((icons) => icons.map((icon) => getComputedStyle(icon).color));
25292529
expect(new Set(groupIconColors).size).toBe(1);
25302530
expect(groupIconColors[0]).not.toBe("");
2531+
const groupIconLayout = await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] .object-vector-studio-v2__object-tile-shape-row").first().evaluate((row) => {
2532+
const label = row.querySelector(".object-vector-studio-v2__shape-select-label");
2533+
const deleteButton = row.querySelector("[data-shape-delete-index]");
2534+
const groupIcon = row.querySelector("[data-shape-group-id='group-1']");
2535+
const rowRect = row.getBoundingClientRect();
2536+
const labelRect = label.getBoundingClientRect();
2537+
const deleteRect = deleteButton.getBoundingClientRect();
2538+
const groupRect = groupIcon.getBoundingClientRect();
2539+
return {
2540+
groupAfterActions: groupRect.left > deleteRect.right,
2541+
groupAtFarRight: Math.abs(rowRect.right - groupRect.right) <= 4,
2542+
labelStartsLeft: labelRect.left < deleteRect.left,
2543+
labelText: label.textContent.trim()
2544+
};
2545+
});
2546+
expect(groupIconLayout).toEqual({
2547+
groupAfterActions: true,
2548+
groupAtFarRight: true,
2549+
labelStartsLeft: true,
2550+
labelText: "1. Circle"
2551+
});
25312552
await page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-tile-shape-index='0']").click();
25322553
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-tile-shape-index='0']")).toHaveAttribute("aria-pressed", "true");
25332554
await expect(page.locator(".object-vector-studio-v2__object-tile[data-object-id='object.asteroids.object-1'] [data-object-tile-shape-index='1']")).toHaveAttribute("aria-pressed", "true");
@@ -3433,6 +3454,22 @@ test.describe("Workspace Manager V2 bootstrap", () => {
34333454
style: { fill: "none", fillOpacity: 1, stroke: "#ffffff", strokeOpacity: 1, strokeWidth: 1 },
34343455
transform: { origin: { x: -35, y: 40 }, rotation: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 },
34353456
visible: true
3457+
},
3458+
{
3459+
geometry: {
3460+
points: [
3461+
{ x: 10, y: 10 },
3462+
{ x: 42, y: 12 },
3463+
{ x: 36, y: 42 },
3464+
{ x: 8, y: 36 }
3465+
]
3466+
},
3467+
tool: "polygon",
3468+
locked: false,
3469+
order: 3,
3470+
style: { fill: "#ffffff", fillOpacity: 1, stroke: "#6fd3ff", strokeOpacity: 1, strokeWidth: 1 },
3471+
transform: { origin: { x: 25, y: 25 }, rotation: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 },
3472+
visible: true
34363473
}
34373474
],
34383475
tags: []
@@ -3445,6 +3482,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
34453482
name: "object-vector-mouse-edit.json"
34463483
});
34473484
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='0']")).toHaveClass(/is-selected/);
3485+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-geometry-point-kind='rectangle-corner']")).toHaveCount(4);
34483486

34493487
const rectangleBeforeDrag = await shapeSnapshot(0);
34503488
await dragLocator("#objectVectorStudioV2RenderSurface [data-shape-index='0']", 44, 24);
@@ -3461,20 +3499,37 @@ test.describe("Workspace Manager V2 bootstrap", () => {
34613499
await expect(page.locator("#statusLog")).toHaveValue(/OK Dragged shape row 0 by/);
34623500

34633501
const rectangleBeforeResize = await shapeSnapshot(0);
3464-
await dragLocator("#objectVectorStudioV2RenderSurface [data-resize-handle='se']", 28, 20);
3502+
await dragLocator("#objectVectorStudioV2RenderSurface [data-geometry-point-handle='rectangle-se']", 28, 20);
34653503
const rectangleAfterResize = await shapeSnapshot(0);
34663504
expect(rectangleAfterResize.geometry.width).toBeGreaterThan(rectangleBeforeResize.geometry.width);
34673505
expect(rectangleAfterResize.geometry.height).toBeGreaterThan(rectangleBeforeResize.geometry.height);
34683506
await expect(page.locator("#statusLog")).toHaveValue(/OK Resized shape row 0 with se handle\./);
34693507

34703508
await page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='1']").click();
34713509
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-line-endpoint='end']")).toHaveCount(1);
3510+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-geometry-point-handle='line-end']")).toHaveCount(1);
34723511
const lineBeforeEndpoint = await shapeSnapshot(1);
34733512
await dragLocator("#objectVectorStudioV2RenderSurface [data-line-endpoint='end']", 36, -18);
34743513
const lineAfterEndpoint = await shapeSnapshot(1);
34753514
expect(lineAfterEndpoint.geometry.point2.x).not.toBe(lineBeforeEndpoint.geometry.point2.x);
34763515
expect(lineAfterEndpoint.geometry.point2.y).not.toBe(lineBeforeEndpoint.geometry.point2.y);
34773516
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved line end for shape row 1\./);
3517+
3518+
await page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='2']").click();
3519+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-geometry-point-kind='polygon-point']")).toHaveCount(4);
3520+
const polygonBeforePointDrag = await shapeSnapshot(2);
3521+
await dragLocator("#objectVectorStudioV2RenderSurface [data-geometry-point-handle='polygon-1']", 24, -16);
3522+
const polygonAfterPointDrag = await shapeSnapshot(2);
3523+
expect(polygonAfterPointDrag.geometry.points[1].x).not.toBe(polygonBeforePointDrag.geometry.points[1].x);
3524+
expect(polygonAfterPointDrag.geometry.points[1].y).not.toBe(polygonBeforePointDrag.geometry.points[1].y);
3525+
await expect(page.locator("#objectVectorStudioV2ObjectDetails [data-polygon-point-index='1'][data-polygon-point-axis='x']")).toHaveValue(String(polygonAfterPointDrag.geometry.points[1].x));
3526+
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved geometry point 2 for shape row 2\./);
3527+
const polygonBeforeBoundsDrag = await shapeSnapshot(2);
3528+
await dragLocator("#objectVectorStudioV2RenderSurface [data-resize-handle='se']", 18, 14);
3529+
const polygonAfterBoundsDrag = await shapeSnapshot(2);
3530+
expect(polygonAfterBoundsDrag.geometry.points[2].x).toBeGreaterThan(polygonBeforeBoundsDrag.geometry.points[2].x);
3531+
expect(polygonAfterBoundsDrag.geometry.points[2].y).toBeGreaterThan(polygonBeforeBoundsDrag.geometry.points[2].y);
3532+
await expect(page.locator("#statusLog")).toHaveValue(/OK Resized shape row 2 with se handle\./);
34783533
await page.evaluate(() => {
34793534
const app = window.__objectVectorStudioV2App;
34803535
const object = app.selectedObject();
@@ -3495,6 +3550,11 @@ test.describe("Workspace Manager V2 bootstrap", () => {
34953550
shapeIndex: 1,
34963551
transform: { ...object.shapes[1].transform },
34973552
visible: true
3553+
},
3554+
{
3555+
shapeIndex: 2,
3556+
transform: { ...object.shapes[2].transform },
3557+
visible: true
34983558
}
34993559
]
35003560
}
@@ -3506,7 +3566,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
35063566
app.renderSelectedObject();
35073567
});
35083568

3509-
const shapeDeleteIconState = await page.locator("[data-shape-delete-index='1']").evaluate((button) => {
3569+
const shapeDeleteIconState = await page.locator("[data-shape-delete-index='2']").evaluate((button) => {
35103570
const icon = button.querySelector("[data-ovs-icon]");
35113571
return {
35123572
objectId: button.dataset.shapeDeleteObjectId,
@@ -3521,10 +3581,12 @@ test.describe("Workspace Manager V2 bootstrap", () => {
35213581
iconKey: "delete",
35223582
iconName: "nf-md-trash_can_outline"
35233583
});
3524-
await page.locator("[data-shape-delete-index='1']").click();
3525-
await expect(page.locator("[data-object-tile-shape-index='1']")).toHaveCount(0);
3584+
await page.locator("[data-shape-delete-index='2']").click();
3585+
await expect(page.locator("[data-object-tile-shape-index='2']")).toHaveCount(0);
3586+
await expect(page.locator("[data-object-tile-shape-index='1']")).toHaveCount(1);
35263587
await expect(page.locator("[data-object-tile-shape-index='0']")).toHaveCount(1);
3527-
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='1']")).toHaveCount(0);
3588+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='2']")).toHaveCount(0);
3589+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='1']")).toHaveCount(1);
35283590
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='0']")).toHaveCount(1);
35293591
const shapeReferenceCleanup = await page.evaluate(() => {
35303592
const payload = window.__objectVectorStudioV2App.currentPayload;
@@ -3534,8 +3596,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
35343596
shapeOverrideIndexes: object.states[0].frames[0].shapeOverrides.map((override) => override.shapeIndex)
35353597
};
35363598
});
3537-
expect(shapeReferenceCleanup).toEqual({ schemaOk: true, shapeOverrideIndexes: [0] });
3538-
await expect(page.locator("#statusLog")).toHaveValue(/OK Deleted shape row 1 from object tile shape delete\./);
3599+
expect(shapeReferenceCleanup).toEqual({ schemaOk: true, shapeOverrideIndexes: [0, 1] });
3600+
await expect(page.locator("#statusLog")).toHaveValue(/OK Deleted shape row 2 from object tile shape delete\./);
35393601

35403602
expect(pageErrors).toEqual([]);
35413603
expect(consoleErrors).toEqual([]);
@@ -8324,6 +8386,18 @@ test.describe("Workspace Manager V2 bootstrap", () => {
83248386
}
83258387
return dirtySession;
83268388
}
8389+
async function dragPreviewLocator(selector, deltaX, deltaY) {
8390+
const target = page.locator(selector);
8391+
await target.scrollIntoViewIfNeeded();
8392+
const box = await target.boundingBox();
8393+
expect(box).not.toBeNull();
8394+
const x = box.x + box.width / 2;
8395+
const y = box.y + box.height / 2;
8396+
await page.mouse.move(x, y);
8397+
await page.mouse.down();
8398+
await page.mouse.move(x + deltaX, y + deltaY, { steps: 4 });
8399+
await page.mouse.up();
8400+
}
83278401

83288402
await page.locator('.object-vector-studio-v2__object-tile[data-object-id="object.asteroids.large-asteroid"] .object-vector-studio-v2__object-select').click();
83298403
await page.locator("#objectVectorStudioV2ZoomInButton").click();
@@ -8360,6 +8434,13 @@ test.describe("Workspace Manager V2 bootstrap", () => {
83608434
await page.locator("#objectVectorStudioV2ObjectDetails [data-shape-geometry-field='points'][data-polygon-point-index='0'][data-polygon-point-axis='x']").fill("11");
83618435
await page.locator("#objectVectorStudioV2ApplyGeometryButton").click();
83628436
});
8437+
await expectObjectVectorDirtyAfter("object geometry point handle drag edit", async () => {
8438+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-geometry-point-handle='polygon-0']")).toHaveCount(1);
8439+
const pointBefore = await page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().geometry.points[0] }));
8440+
await dragPreviewLocator("#objectVectorStudioV2RenderSurface [data-geometry-point-handle='polygon-0']", 18, 12);
8441+
const pointAfter = await page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().geometry.points[0] }));
8442+
expect(pointAfter).not.toEqual(pointBefore);
8443+
});
83638444
await expectObjectVectorDirtyAfter("object transform edit", async () => {
83648445
await page.locator("#objectVectorStudioV2MoveXInput").fill("5");
83658446
await page.locator("#objectVectorStudioV2MoveYInput").fill("-5");

0 commit comments

Comments
 (0)