Skip to content

Commit d38eb10

Browse files
author
DavidQ
committed
Fix Object Vector Studio transformed selection bounds and hit testing - PR_26133_037-object-preview-transform-bounds-fix
1 parent d7bda58 commit d38eb10

5 files changed

Lines changed: 300 additions & 34 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# PR_26133_037 Object Preview Transform Bounds Report
2+
3+
Task: PR_26133_037-object-preview-transform-bounds-fix
4+
Date: 2026-05-15
5+
6+
## Implementation
7+
8+
- Added transformed geometry point collection for Object Vector Studio V2 preview bounds.
9+
- Added a point transform helper that mirrors the SVG transform order used for rendering:
10+
`translate(x y)`, `translate(origin)`, `rotate`, `scale`, `translate(-origin)`.
11+
- Updated `transformedBounds` to union transformed points instead of scaling raw axis-aligned bounds.
12+
- Bounds now include x/y transform, rotation, scaleX/scaleY, and origin.
13+
- Selection box and resize handles now use transformed bounds.
14+
- Line endpoint handles now use transformed endpoint positions instead of raw endpoint plus x/y offset.
15+
- Object-level bounds continue to use `transformedBounds`, so object bounds inherit the same transformed geometry contract.
16+
17+
## Geometry Coverage
18+
19+
- Rectangle/text: transformed corner points.
20+
- Line: transformed point1 and point2.
21+
- Polygon/triangle: transformed point list.
22+
- Circle/ellipse: sampled perimeter points before applying shape transform.
23+
- Arc: sampled arc points before applying shape transform.
24+
25+
## Validation
26+
27+
- `npm run test:workspace-v2` passed with 50 tests.
28+
- Added Playwright coverage confirms a scaled and rotated selected shape has:
29+
- dashed selection bounds matching transformed geometry,
30+
- four handles aligned to transformed bounds,
31+
- hit testing on transformed visual geometry outside raw bounds,
32+
- drag interaction startup from the transformed visual region.
33+
- No raw-geometry-only selection fallback was added.

docs/dev/reports/playwright_v8_coverage_report.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_036 Playwright V8 Coverage Report
1+
# PR_26133_037 Playwright V8 Coverage Report
22

3-
Task: PR_26133_036-asteroids-manifest-name-validation-no-fallback
3+
Task: PR_26133_037-object-preview-transform-bounds-fix
44
Date: 2026-05-15
55

66
## Result
@@ -26,14 +26,13 @@ PASS - Coverage reporting was generated during `npm run test:workspace-v2`.
2626
## Changed Runtime JS Coverage
2727

2828
```text
29-
(97%) src/engine/rendering/ObjectVectorRuntimeAssetService.js - executed lines 1145/1145; executed functions 123/127
29+
(94%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 4416/4416; executed functions 466/498
3030
```
3131

32-
Additional changed Asteroids runtime modules were exercised by the Workspace V2 Asteroids gameplay rendering test:
32+
## Changed Test Coverage Note
3333

3434
```text
35-
(50%) games/Asteroids/index.js - executed lines 211/211; executed functions 6/12
36-
(52%) games/Asteroids/game/AsteroidsGameScene.js - executed lines 874/874; executed functions 26/50
35+
(0%) tests/playwright/tools/WorkspaceManagerV2.spec.mjs - changed JS file not collected as browser runtime coverage
3736
```
3837

3938
## Guardrail
@@ -44,4 +43,4 @@ Additional changed Asteroids runtime modules were exercised by the Workspace V2
4443

4544
## PR-Specific Note
4645

47-
The Workspace V2 run exercised Asteroids Object Vector manifest loading, strict runtime binding validation, duplicate medium-candidate diagnostics, and gameplay rendering with manifest-selected object IDs. Coverage remains advisory only.
46+
The Workspace V2 run exercised Object Vector Studio V2 transformed selection bounds, transformed handle placement, transformed hit testing, and drag interaction startup from the transformed visual shape region. Coverage remains advisory only.
Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# PR_26133_036 Workspace V2 Playwright Results
1+
# PR_26133_037 Workspace V2 Playwright Results
22

3-
Task: PR_26133_036-asteroids-manifest-name-validation-no-fallback
3+
Task: PR_26133_037-object-preview-transform-bounds-fix
44
Date: 2026-05-15
55

66
## Result
@@ -9,21 +9,17 @@ PASS - `npm run test:workspace-v2` completed successfully.
99

1010
- Command: `npm run test:workspace-v2`
1111
- Playwright target: `tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list`
12-
- Final result: 49 passed, 0 failed.
13-
- Runtime/console guard: Workspace V2 tests that monitor page errors completed with no reported page errors.
12+
- Final result: 50 passed, 0 failed.
13+
- Runtime/console guard: Object Vector Studio V2 tests that monitor page errors and console errors completed with no reported errors.
1414

1515
## PR-Specific Coverage
1616

17-
- Asteroids runtime loaded Object Vector Studio V2 assets from `games/Asteroids/game.manifest.json`.
18-
- Runtime binding diagnostics reported `runtimeBindingsValid: true`.
19-
- Runtime selected `object.asteroids.medium-asteroid` from manifest binding even with duplicate medium-tag candidates.
20-
- Object Vector Studio V2 continued to load/save 7 Asteroids object-vector objects.
21-
- No `BASE_VECTOR_MAP`, `ASTEROIDS_*_SVG`, or `vector.asteroids.*` runtime fallback references remain in Asteroids/runtime shared paths.
17+
- Added `aligns Object Vector Studio V2 selection bounds to transformed preview geometry`.
18+
- Verified the dashed selection rectangle is calculated from transformed geometry corners, including x/y, rotation, scaleX/scaleY, and origin.
19+
- Verified all four resize handles align to the transformed selection bounds.
20+
- Verified the transformed shape center is outside the raw geometry bounds but still hit-tests against the selected SVG shape.
21+
- Verified drag interaction can start from the transformed visual region and updates the selected shape transform.
2222

23-
## Additional Validation
23+
## Manual Verification Equivalent
2424

25-
PASS - Targeted Asteroids Platform Demo test.
26-
27-
PASS - Targeted Asteroids Asset Reference Adoption test.
28-
29-
PASS - Targeted manifest binding validation confirmed valid bindings pass, missing `asteroidMedium` fails, and invalid `asteroidMedium` tag binding fails.
25+
Targeted Object Vector Studio V2 browser automation covered the requested scaled/rotated selection behavior, transformed hit testing, drag interaction continuity, and no-console-error checks.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3227,6 +3227,170 @@ test.describe("Workspace Manager V2 bootstrap", () => {
32273227
}
32283228
});
32293229

3230+
test("aligns Object Vector Studio V2 selection bounds to transformed preview geometry", async ({ page }) => {
3231+
const server = await startRepoServer();
3232+
const pageErrors = [];
3233+
const consoleErrors = [];
3234+
3235+
page.on("pageerror", (error) => {
3236+
pageErrors.push(error.message);
3237+
});
3238+
page.on("console", (message) => {
3239+
if (message.type() === "error") {
3240+
consoleErrors.push(message.text());
3241+
}
3242+
});
3243+
3244+
await coverageReporter.start(page);
3245+
try {
3246+
await page.setViewportSize({ width: 1366, height: 1000 });
3247+
await page.goto(`${server.baseUrl}/tools/object-vector-studio-v2/index.html`, { waitUntil: "networkidle" });
3248+
await page.evaluate(() => {
3249+
sessionStorage.setItem("object-vector-studio-v2.runtimePalette", JSON.stringify({
3250+
id: "transform-bounds-palette",
3251+
swatches: [
3252+
{ id: "white", value: "#ffffff" },
3253+
{ id: "blue", value: "#60a5fa" }
3254+
]
3255+
}));
3256+
});
3257+
await page.locator("#objectVectorStudioV2ImportJsonInput").setInputFiles({
3258+
buffer: Buffer.from(JSON.stringify({
3259+
name: "Transform Bounds Object Set",
3260+
objects: [
3261+
{
3262+
id: "object.bounds.transformed-rectangle",
3263+
name: "Transformed Rectangle",
3264+
shapes: [
3265+
{
3266+
geometry: { height: 40, width: 80, x: -40, y: -20 },
3267+
locked: false,
3268+
order: 1,
3269+
style: { fill: "#ffffff", fillOpacity: 1, stroke: "#60a5fa", strokeOpacity: 1, strokeWidth: 1 },
3270+
tool: "rectangle",
3271+
transform: { origin: { x: -10, y: 5 }, rotation: 30, scaleX: 0.5, scaleY: 0.5, x: 60, y: 10 },
3272+
visible: true
3273+
}
3274+
],
3275+
tags: []
3276+
}
3277+
],
3278+
toolId: "object-vector-studio-v2",
3279+
version: 1
3280+
}, null, 2)),
3281+
mimeType: "application/json",
3282+
name: "object-vector-transform-bounds.json"
3283+
});
3284+
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='0']")).toHaveClass(/is-selected/);
3285+
3286+
const metrics = await page.locator("#objectVectorStudioV2RenderSurface").evaluate((surface) => {
3287+
const drawingScale = 10;
3288+
const app = window.__objectVectorStudioV2App;
3289+
const shape = app.selectedShape();
3290+
const transform = shape.transform;
3291+
const geometry = shape.geometry;
3292+
const transformPoint = (point) => {
3293+
const radians = transform.rotation * Math.PI / 180;
3294+
const relativeX = (point.x - transform.origin.x) * transform.scaleX;
3295+
const relativeY = (point.y - transform.origin.y) * transform.scaleY;
3296+
const rotatedX = relativeX * Math.cos(radians) - relativeY * Math.sin(radians);
3297+
const rotatedY = relativeX * Math.sin(radians) + relativeY * Math.cos(radians);
3298+
return {
3299+
x: (transform.x + transform.origin.x + rotatedX) * drawingScale,
3300+
y: (transform.y + transform.origin.y + rotatedY) * drawingScale
3301+
};
3302+
};
3303+
const corners = [
3304+
{ x: geometry.x, y: geometry.y },
3305+
{ x: geometry.x + geometry.width, y: geometry.y },
3306+
{ x: geometry.x + geometry.width, y: geometry.y + geometry.height },
3307+
{ x: geometry.x, y: geometry.y + geometry.height }
3308+
].map(transformPoint);
3309+
const xValues = corners.map((point) => point.x);
3310+
const yValues = corners.map((point) => point.y);
3311+
const expected = {
3312+
center: transformPoint({ x: geometry.x + geometry.width / 2, y: geometry.y + geometry.height / 2 }),
3313+
height: Math.max(...yValues) - Math.min(...yValues),
3314+
width: Math.max(...xValues) - Math.min(...xValues),
3315+
x: Math.min(...xValues),
3316+
y: Math.min(...yValues)
3317+
};
3318+
const selectionBox = surface.querySelector("[data-selection-bounds='0']");
3319+
const handles = Object.fromEntries(Array.from(surface.querySelectorAll("[data-resize-handle]")).map((handle) => [
3320+
handle.dataset.resizeHandle,
3321+
{
3322+
cx: Number(handle.getAttribute("x")) + Number(handle.getAttribute("width")) / 2,
3323+
cy: Number(handle.getAttribute("y")) + Number(handle.getAttribute("height")) / 2
3324+
}
3325+
]));
3326+
const raw = {
3327+
height: geometry.height * drawingScale,
3328+
width: geometry.width * drawingScale,
3329+
x: geometry.x * drawingScale,
3330+
y: geometry.y * drawingScale
3331+
};
3332+
const svgPoint = surface.createSVGPoint();
3333+
svgPoint.x = expected.center.x;
3334+
svgPoint.y = expected.center.y;
3335+
const screenCenter = svgPoint.matrixTransform(surface.getScreenCTM());
3336+
const hitShape = document.elementFromPoint(screenCenter.x, screenCenter.y)?.closest("[data-shape-index]");
3337+
3338+
return {
3339+
expected,
3340+
handles,
3341+
hitShapeIndex: hitShape?.dataset.shapeIndex || null,
3342+
raw,
3343+
rawContainsTransformedCenter: expected.center.x >= raw.x
3344+
&& expected.center.x <= raw.x + raw.width
3345+
&& expected.center.y >= raw.y
3346+
&& expected.center.y <= raw.y + raw.height,
3347+
screenCenter: { x: screenCenter.x, y: screenCenter.y },
3348+
selection: {
3349+
height: Number(selectionBox.getAttribute("height")),
3350+
width: Number(selectionBox.getAttribute("width")),
3351+
x: Number(selectionBox.getAttribute("x")),
3352+
y: Number(selectionBox.getAttribute("y"))
3353+
}
3354+
};
3355+
});
3356+
3357+
expect(metrics.rawContainsTransformedCenter).toBe(false);
3358+
expect(metrics.hitShapeIndex).toBe("0");
3359+
expect(metrics.selection.x).toBeCloseTo(metrics.expected.x - 4, 3);
3360+
expect(metrics.selection.y).toBeCloseTo(metrics.expected.y - 4, 3);
3361+
expect(metrics.selection.width).toBeCloseTo(metrics.expected.width + 8, 3);
3362+
expect(metrics.selection.height).toBeCloseTo(metrics.expected.height + 8, 3);
3363+
expect(metrics.selection.width).toBeLessThan(metrics.raw.width);
3364+
3365+
const expectedHandleCenters = {
3366+
ne: { x: metrics.expected.x + metrics.expected.width + 4, y: metrics.expected.y - 4 },
3367+
nw: { x: metrics.expected.x - 4, y: metrics.expected.y - 4 },
3368+
se: { x: metrics.expected.x + metrics.expected.width + 4, y: metrics.expected.y + metrics.expected.height + 4 },
3369+
sw: { x: metrics.expected.x - 4, y: metrics.expected.y + metrics.expected.height + 4 }
3370+
};
3371+
for (const [handle, expectedCenter] of Object.entries(expectedHandleCenters)) {
3372+
expect(metrics.handles[handle].cx).toBeCloseTo(expectedCenter.x, 3);
3373+
expect(metrics.handles[handle].cy).toBeCloseTo(expectedCenter.y, 3);
3374+
}
3375+
3376+
const transformBeforeDrag = await page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().transform }));
3377+
await page.mouse.move(metrics.screenCenter.x, metrics.screenCenter.y);
3378+
await page.mouse.down();
3379+
await page.mouse.move(metrics.screenCenter.x + 40, metrics.screenCenter.y + 24, { steps: 4 });
3380+
await page.mouse.up();
3381+
const transformAfterDrag = await page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().transform }));
3382+
expect(transformAfterDrag.x).not.toBe(transformBeforeDrag.x);
3383+
expect(transformAfterDrag.y).not.toBe(transformBeforeDrag.y);
3384+
await expect(page.locator("#statusLog")).toHaveValue(/OK Dragged shape row 0 by/);
3385+
3386+
expect(pageErrors).toEqual([]);
3387+
expect(consoleErrors).toEqual([]);
3388+
} finally {
3389+
await coverageReporter.stop(page);
3390+
await server.close();
3391+
}
3392+
});
3393+
32303394
test("expands Object Vector Studio V2 asset authoring controls", async ({ page }, testInfo) => {
32313395
const server = await startRepoServer();
32323396
const pageErrors = [];

0 commit comments

Comments
 (0)