Skip to content

Commit de8870f

Browse files
author
DavidQ
committed
Make Rotate operate on selected groups in Object Vector Studio V2 - PR_26133_057-group-rotate-transform-behavior
1 parent ceaf496 commit de8870f

5 files changed

Lines changed: 315 additions & 67 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_056 Playwright V8 Coverage Report
1+
# PR_26133_057 Playwright V8 Coverage Report
22

3-
Task: PR_26133_056-object-preview-pointer-zoom-and-shape-tile-actions
3+
Task: PR_26133_057-group-rotate-transform-behavior
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 5583/5583; executed functions 590/621
27+
(95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 5676/5676; executed functions 599/631
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 pointer-anchored wheel zoom, off-center viewport origin updates, shape tile group/eye/trash action event handling, targeted visibility toggles, shape delete behavior, schema validation, and Asteroids runtime object-vector rendering.
38+
The Workspace V2 run exercised Object Vector Studio V2 group-aware Rotate behavior, grouped transform validation, selected-shape independent Rotate behavior, refreshed preview bounds/handles, schema validation, and Asteroids runtime object-vector rendering.
Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_056 Workspace V2 Playwright Results
1+
# PR_26133_057 Workspace V2 Playwright Results
22

3-
Task: PR_26133_056-object-preview-pointer-zoom-and-shape-tile-actions
3+
Task: PR_26133_057-group-rotate-transform-behavior
44
Date: 2026-05-15
55

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

1515
## PR-Specific Coverage
1616

17-
- Verified mouse-wheel zoom preserves the world coordinate under the pointer instead of zooming only from canvas center.
18-
- Verified off-center wheel zoom updates viewport origin and keeps existing zoom limits/display behavior.
19-
- Verified shape row layout renders as shape label, group button, eye button, and trash button.
20-
- Verified group/eye/trash controls are sibling buttons and are not nested inside the shape selection button.
21-
- Verified clicking the group button does not select the shape.
22-
- Verified clicking the eye action toggles the targeted shape visibility without selecting it.
23-
- Verified clicking the trash action deletes the targeted shape without selecting it first.
17+
- Verified Rotate applies to every shape in the selected group when the selected shape belongs to a valid group.
18+
- Verified group rotation preserves relative origin spacing while rotating around the selected shape pivot/origin.
19+
- Verified non-grouped shapes keep the existing independent Rotate behavior.
20+
- Verified preview rendering, selection bounds, and resize handles refresh after grouped rotation.
2421

2522
## Additional Validation
2623

27-
- Focused pointer-zoom/shape-tile layout slice passed:
28-
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "layout shell"` completed with 1 passed, 0 failed.
29-
- Focused preview delete/action slice passed:
30-
`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.
31-
- `git diff --check` passed.
24+
- Focused group/state transform slice passed:
25+
`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.
26+
- `git diff --check` passed. The command reported the existing Windows LF-to-CRLF warning for `tests/playwright/tools/WorkspaceManagerV2.spec.mjs` and no whitespace errors.

games/Asteroids/game.manifest.json

Lines changed: 143 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -218,29 +218,19 @@
218218
"name": "Asteroids Ship",
219219
"shapes": [
220220
{
221-
"tool": "polygon",
222-
"order": 0,
221+
"tool": "line",
222+
"order": 1,
223223
"visible": true,
224224
"locked": false,
225225
"geometry": {
226-
"points": [
227-
{
228-
"x": 0,
229-
"y": -18
230-
},
231-
{
232-
"x": 14,
233-
"y": 16
234-
},
235-
{
236-
"x": 0,
237-
"y": 8
238-
},
239-
{
240-
"x": -14,
241-
"y": 16
242-
}
243-
]
226+
"point1": {
227+
"x": 4,
228+
"y": 10
229+
},
230+
"point2": {
231+
"x": 0,
232+
"y": 16
233+
}
244234
},
245235
"style": {
246236
"fill": "transparent",
@@ -256,24 +246,25 @@
256246
"scaleX": 1,
257247
"scaleY": 1,
258248
"origin": {
259-
"x": 0,
260-
"y": 0
249+
"x": 3,
250+
"y": 10
261251
}
262-
}
252+
},
253+
"groupId": "group-1"
263254
},
264255
{
265256
"tool": "line",
266-
"order": 1,
257+
"order": 2,
267258
"visible": true,
268259
"locked": false,
269260
"geometry": {
270261
"point1": {
271-
"x": -6,
272-
"y": 14
262+
"x": -4,
263+
"y": 10
273264
},
274265
"point2": {
275266
"x": 0,
276-
"y": 6
267+
"y": 16
277268
}
278269
},
279270
"style": {
@@ -293,25 +284,36 @@
293284
"x": -3,
294285
"y": 10
295286
}
296-
}
287+
},
288+
"groupId": "group-1"
297289
},
298290
{
299-
"tool": "line",
300-
"order": 2,
291+
"tool": "polygon",
292+
"order": 3,
301293
"visible": true,
302294
"locked": false,
303295
"geometry": {
304-
"point1": {
305-
"x": 6,
306-
"y": 14
307-
},
308-
"point2": {
309-
"x": 0,
310-
"y": 6
311-
}
296+
"points": [
297+
{
298+
"x": -0.694,
299+
"y": -17.218
300+
},
301+
{
302+
"x": 14,
303+
"y": 16
304+
},
305+
{
306+
"x": 0,
307+
"y": 7.5
308+
},
309+
{
310+
"x": -14,
311+
"y": 16
312+
}
313+
]
312314
},
313315
"style": {
314-
"fill": "transparent",
316+
"fill": "#05070D",
315317
"stroke": "#FFFFFF",
316318
"strokeWidth": 2,
317319
"fillOpacity": 1,
@@ -324,8 +326,8 @@
324326
"scaleX": 1,
325327
"scaleY": 1,
326328
"origin": {
327-
"x": 3,
328-
"y": 10
329+
"x": 0,
330+
"y": 0
329331
}
330332
}
331333
}
@@ -341,12 +343,49 @@
341343
"durationFrames": 1,
342344
"shapeOverrides": [
343345
{
344-
"visible": false,
345-
"shapeIndex": 1
346+
"visible": true,
347+
"shapeIndex": 1,
348+
"transform": {
349+
"x": 0.002,
350+
"y": 0,
351+
"rotation": 0,
352+
"scaleX": 1,
353+
"scaleY": 1,
354+
"origin": {
355+
"x": -3,
356+
"y": 10
357+
}
358+
}
346359
},
347360
{
348-
"visible": false,
349-
"shapeIndex": 2
361+
"visible": true,
362+
"shapeIndex": 0,
363+
"transform": {
364+
"x": 0,
365+
"y": 0,
366+
"rotation": 0,
367+
"scaleX": 1,
368+
"scaleY": 1,
369+
"origin": {
370+
"x": 0,
371+
"y": 0
372+
}
373+
}
374+
},
375+
{
376+
"shapeIndex": 2,
377+
"transform": {
378+
"x": 0,
379+
"y": 0,
380+
"rotation": 0,
381+
"scaleX": 1,
382+
"scaleY": 1,
383+
"origin": {
384+
"x": 0,
385+
"y": 0
386+
}
387+
},
388+
"visible": true
350389
}
351390
]
352391
}
@@ -367,11 +406,69 @@
367406
},
368407
{
369408
"visible": true,
370-
"shapeIndex": 2
409+
"shapeIndex": 0
371410
}
372411
]
373412
}
374413
]
414+
},
415+
{
416+
"frames": [
417+
{
418+
"durationFrames": 1,
419+
"id": "frame-1",
420+
"order": 1,
421+
"shapeOverrides": [
422+
{
423+
"shapeIndex": 2,
424+
"transform": {
425+
"x": 0,
426+
"y": 0,
427+
"rotation": 0,
428+
"scaleX": 1,
429+
"scaleY": 1,
430+
"origin": {
431+
"x": 0,
432+
"y": 0
433+
}
434+
},
435+
"visible": true
436+
},
437+
{
438+
"shapeIndex": 1,
439+
"transform": {
440+
"x": 0,
441+
"y": 0,
442+
"rotation": 0,
443+
"scaleX": 1,
444+
"scaleY": 1,
445+
"origin": {
446+
"x": -3,
447+
"y": 10
448+
}
449+
},
450+
"visible": true
451+
},
452+
{
453+
"shapeIndex": 0,
454+
"transform": {
455+
"x": 0,
456+
"y": 0,
457+
"rotation": 0,
458+
"scaleX": 1,
459+
"scaleY": 1,
460+
"origin": {
461+
"x": 3,
462+
"y": 10
463+
}
464+
},
465+
"visible": true
466+
}
467+
]
468+
}
469+
],
470+
"id": "destroyed",
471+
"name": "Destroyed"
375472
}
376473
],
377474
"tags": [

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4415,7 +4415,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
44154415
order: 3,
44164416
style: { fill: "#ffffff", fillOpacity: 1, stroke: "#6fd3ff", strokeOpacity: 1, strokeWidth: 2 },
44174417
tool: "rectangle",
4418-
transform: { origin: { x: 0, y: 0 }, rotation: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 },
4418+
transform: { origin: { x: 0, y: 0 }, rotation: 0, scaleX: 1, scaleY: 1, x: 30, y: 0 },
44194419
visible: true
44204420
},
44214421
{
@@ -4507,7 +4507,44 @@ test.describe("Workspace Manager V2 bootstrap", () => {
45074507
return { x: transform.x, y: transform.y };
45084508
});
45094509
});
4510-
expect(transformsAfterGroupMove).toEqual([{ x: 5, y: 5 }, { x: 0, y: 0 }, { x: 5, y: 5 }, { x: 0, y: 0 }]);
4510+
expect(transformsAfterGroupMove).toEqual([{ x: 5, y: 5 }, { x: 0, y: 0 }, { x: 35, y: 5 }, { x: 0, y: 0 }]);
4511+
4512+
const selectionBoundsBeforeGroupRotate = await page.locator("#objectVectorStudioV2RenderSurface [data-selection-bounds='0']").evaluate((box) => ({
4513+
height: Number(box.getAttribute("height")),
4514+
width: Number(box.getAttribute("width")),
4515+
x: Number(box.getAttribute("x")),
4516+
y: Number(box.getAttribute("y"))
4517+
}));
4518+
await page.locator("#objectVectorStudioV2RotateInput").fill("90");
4519+
await page.locator("#objectVectorStudioV2RotateShapeButton").click();
4520+
await expect(page.locator("#statusLog")).toHaveValue(/OK Rotated group group-2 \(2 shapes\) by 90 degrees\./);
4521+
const transformsAfterGroupRotate = await page.evaluate(() => {
4522+
const app = window.__objectVectorStudioV2App;
4523+
const frame = app.activeFrame();
4524+
return app.selectedObject().shapes.map((shape, shapeIndex) => {
4525+
const transform = app.effectiveShapeForFrame(shape, frame, shapeIndex).transform;
4526+
return { rotation: transform.rotation, x: transform.x, y: transform.y };
4527+
});
4528+
});
4529+
expect(transformsAfterGroupRotate).toEqual([
4530+
{ rotation: 90, x: 5, y: 5 },
4531+
{ rotation: 0, x: 0, y: 0 },
4532+
{ rotation: 90, x: 5, y: 35 },
4533+
{ rotation: 0, x: 0, y: 0 }
4534+
]);
4535+
const groupRotateOriginDistance = Math.hypot(
4536+
transformsAfterGroupRotate[2].x - transformsAfterGroupRotate[0].x,
4537+
transformsAfterGroupRotate[2].y - transformsAfterGroupRotate[0].y
4538+
);
4539+
expect(groupRotateOriginDistance).toBeCloseTo(30, 3);
4540+
const selectionBoundsAfterGroupRotate = await page.locator("#objectVectorStudioV2RenderSurface [data-selection-bounds='0']").evaluate((box) => ({
4541+
height: Number(box.getAttribute("height")),
4542+
width: Number(box.getAttribute("width")),
4543+
x: Number(box.getAttribute("x")),
4544+
y: Number(box.getAttribute("y"))
4545+
}));
4546+
expect(selectionBoundsAfterGroupRotate).not.toEqual(selectionBoundsBeforeGroupRotate);
4547+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-resize-handle]")).toHaveCount(4);
45114548

45124549
await page.evaluate(() => window.__objectVectorStudioV2App.selectShape(1, "single move probe"));
45134550
await page.locator("#objectVectorStudioV2MoveXInput").fill("-2");
@@ -4522,7 +4559,25 @@ test.describe("Workspace Manager V2 bootstrap", () => {
45224559
return { x: transform.x, y: transform.y };
45234560
});
45244561
});
4525-
expect(transformsAfterSingleMove).toEqual([{ x: 5, y: 5 }, { x: -2, y: -3 }, { x: 5, y: 5 }, { x: 0, y: 0 }]);
4562+
expect(transformsAfterSingleMove).toEqual([{ x: 5, y: 5 }, { x: -2, y: -3 }, { x: 5, y: 35 }, { x: 0, y: 0 }]);
4563+
4564+
await page.locator("#objectVectorStudioV2RotateInput").fill("45");
4565+
await page.locator("#objectVectorStudioV2RotateShapeButton").click();
4566+
await expect(page.locator("#statusLog")).toHaveValue(/OK Rotated shape row 1 by 45 degrees\./);
4567+
const transformsAfterSingleRotate = await page.evaluate(() => {
4568+
const app = window.__objectVectorStudioV2App;
4569+
const frame = app.activeFrame();
4570+
return app.selectedObject().shapes.map((shape, shapeIndex) => {
4571+
const transform = app.effectiveShapeForFrame(shape, frame, shapeIndex).transform;
4572+
return { rotation: transform.rotation, x: transform.x, y: transform.y };
4573+
});
4574+
});
4575+
expect(transformsAfterSingleRotate).toEqual([
4576+
{ rotation: 90, x: 5, y: 5 },
4577+
{ rotation: 45, x: -2, y: -3 },
4578+
{ rotation: 90, x: 5, y: 35 },
4579+
{ rotation: 0, x: 0, y: 0 }
4580+
]);
45264581

45274582
await page.evaluate(() => {
45284583
const app = window.__objectVectorStudioV2App;

0 commit comments

Comments
 (0)