Skip to content

Commit 8d95c36

Browse files
author
DavidQ
committed
Fix Object Vector V2 snap-to-grid point dragging and coordinate mapping - PR_26133_103-object-vector-snap-drag-fix
1 parent aa1a62f commit 8d95c36

3 files changed

Lines changed: 191 additions & 36 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# PR_26133_103 Object Vector Snap Drag Fix
2+
3+
## Scope
4+
- Read `docs/dev/PROJECT_INSTRUCTIONS.md` before implementation.
5+
- Used `tmp/PR_26133_102-object-vector-scale-anchor-fix_delta.zip` as the prior reference.
6+
- Kept changes limited to Object Vector Studio V2 snap/drag coordinate correction and focused Workspace V2 coverage.
7+
8+
## Implementation
9+
- Fixed geometry point drag snapping so handle movement is target-based instead of `original point + snapped pointer delta`.
10+
- Geometry point and line endpoint drags now:
11+
- preserve the pointer grab offset from the transformed handle point,
12+
- snap the intended handle target through Snap Grid / Snap Point / Snap None,
13+
- convert the snapped world/object point back through the original shape transform with `localPointFromTransformedPoint`,
14+
- update the underlying local geometry so the visible grabbed point lands on the exact snap target.
15+
- This corrects off-grid start overshoot and transformed shape scale/zoom mapping drift.
16+
- Point coordinate UI rows now render fixed three-decimal values such as `47.000` for snapped whole-number coordinates while stored geometry remains numeric.
17+
18+
## Playwright Impact
19+
Playwright impacted: Yes.
20+
21+
Validated behavior:
22+
- Snap Grid point drag from an off-grid polygon point lands exactly on whole-number coordinates.
23+
- Dragged polygon point row displays snapped coordinates as fixed three-decimal values.
24+
- Scaled line endpoint drag converts cursor movement through the shape transform so the rendered endpoint lands exactly on the snapped target.
25+
- Existing preview coordinate grid mapping, mouse editing, and dirty-state flows continue to pass.
26+
27+
Expected pass behavior:
28+
- The dragged point tracks the cursor target and snaps without overshoot.
29+
- Transformed/scaled endpoints visually land on the snapped grid coordinate.
30+
- Workspace V2 suite remains green.
31+
32+
Expected fail behavior:
33+
- A regression would leave polygon point values offset from the snap point, or a scaled line endpoint rendered away from the snapped target.
34+
35+
## Validation
36+
- PASS: `node --check tools\object-vector-studio-v2\js\ToolStarterApp.js`
37+
- PASS: `node --check tests\playwright\tools\WorkspaceManagerV2.spec.mjs`
38+
- PASS: targeted Object Vector V2 snap/drag validation:
39+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "edits Object Vector Studio V2 preview shapes with mouse actions and tile delete controls|maps Object Vector Studio V2 preview coordinates directly to visible grid lines"`
40+
- Result: 2 passed.
41+
- PASS: targeted dirty-state validation:
42+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "tracks Object Vector Studio V2 dirty state through persisted edits and save outcomes"`
43+
- Result: 1 passed.
44+
- PASS: `npm run test:workspace-v2`
45+
- Result: 56 passed.
46+
- PASS: `git diff --check -- tools/object-vector-studio-v2/js/ToolStarterApp.js tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
47+
- Only CRLF warning for the existing WorkspaceManagerV2 Playwright test file.
48+
49+
## Manual Validation Steps
50+
1. Open Object Vector Studio V2 and select an object with polygon or line geometry.
51+
2. Set Snap mode to Snap Grid.
52+
3. Drag an off-grid polygon point handle to a nearby grid intersection.
53+
4. Verify the geometry point lands on the exact snapped coordinate and the point row shows values like `47.000` / `10.000`.
54+
5. Scale a shape, then drag a line endpoint handle.
55+
6. Verify the rendered endpoint tracks the cursor and lands exactly on the snapped target instead of moving too far or too little.
56+
57+
## Full Samples Smoke Test
58+
Skipped per PR_26133_103 instructions; this change is scoped to Object Vector Studio V2 snap/drag behavior.
59+
60+
## PR103 Delta Stat vs PR102 ZIP Baseline
61+
```
62+
tools/object-vector-studio-v2/js/ToolStarterApp.js: +54 -20
63+
tests/playwright/tools/WorkspaceManagerV2.spec.mjs: +73 -16
64+
```

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,28 @@ async function dragObjectVectorLogicalPoints(page, start, end) {
147147
}, { endPoint, startPoint });
148148
}
149149

150+
async function dragObjectVectorHandleToLogicalPoint(page, selector, start, end) {
151+
const startPoint = await objectVectorLogicalClientPoint(page, start.x, start.y);
152+
const endPoint = await objectVectorLogicalClientPoint(page, end.x, end.y);
153+
await page.evaluate(({ endPoint, selector, startPoint }) => {
154+
const handle = document.querySelector(selector);
155+
if (!handle) {
156+
throw new Error(`Missing Object Vector handle: ${selector}`);
157+
}
158+
const pointerInit = {
159+
bubbles: true,
160+
button: 0,
161+
buttons: 1,
162+
cancelable: true,
163+
pointerId: 1,
164+
pointerType: "mouse"
165+
};
166+
handle.dispatchEvent(new PointerEvent("pointerdown", { ...pointerInit, clientX: startPoint.x, clientY: startPoint.y }));
167+
window.dispatchEvent(new PointerEvent("pointermove", { ...pointerInit, clientX: endPoint.x, clientY: endPoint.y }));
168+
window.dispatchEvent(new PointerEvent("pointerup", { ...pointerInit, buttons: 0, clientX: endPoint.x, clientY: endPoint.y }));
169+
}, { endPoint, selector, startPoint });
170+
}
171+
150172
async function mouseDragObjectVectorLogicalPoints(page, start, end) {
151173
const startPoint = await objectVectorLogicalClientPoint(page, start.x, start.y);
152174
const endPoint = await objectVectorLogicalClientPoint(page, end.x, end.y);
@@ -4544,10 +4566,10 @@ test.describe("Workspace Manager V2 bootstrap", () => {
45444566
x: row.querySelector("[data-polygon-point-axis='x']").value,
45454567
y: row.querySelector("[data-polygon-point-axis='y']").value
45464568
})))).toEqual([
4547-
{ label: "Point 1", rounded: false, x: "0", y: "-18" },
4548-
{ label: "Point 2", rounded: false, x: "14", y: "16" },
4549-
{ label: "Point 3", rounded: false, x: "0", y: "8" },
4550-
{ label: "Point 4", rounded: false, x: "-14", y: "16" }
4569+
{ label: "Point 1", rounded: false, x: "0.000", y: "-18.000" },
4570+
{ label: "Point 2", rounded: false, x: "14.000", y: "16.000" },
4571+
{ label: "Point 3", rounded: false, x: "0.000", y: "8.000" },
4572+
{ label: "Point 4", rounded: false, x: "-14.000", y: "16.000" }
45514573
]);
45524574
const polygonPointListLayout = await page.locator("#objectVectorStudioV2ShapeGeometryDetails .object-vector-studio-v2__polygon-point-list").evaluate((list) => ({
45534575
headingMarginBottom: Number.parseFloat(getComputedStyle(list.previousElementSibling).marginBottom),
@@ -4679,11 +4701,11 @@ test.describe("Workspace Manager V2 bootstrap", () => {
46794701
y: row.querySelector("[data-polygon-point-axis='y']").value,
46804702
selected: row.dataset.polygonPointActionSelected === "true"
46814703
})))).toEqual([
4682-
{ label: "Point 1", rounded: false, x: "0", y: "-18", selected: false },
4683-
{ label: "Point 2", rounded: true, x: "14", y: "16", selected: false },
4684-
{ label: "Point 3", rounded: true, x: "14", y: "16", selected: false },
4685-
{ label: "Point 4", rounded: false, x: "0", y: "8", selected: false },
4686-
{ label: "Point 5", rounded: false, x: "-14", y: "16", selected: false }
4704+
{ label: "Point 1", rounded: false, x: "0.000", y: "-18.000", selected: false },
4705+
{ label: "Point 2", rounded: true, x: "14.000", y: "16.000", selected: false },
4706+
{ label: "Point 3", rounded: true, x: "14.000", y: "16.000", selected: false },
4707+
{ label: "Point 4", rounded: false, x: "0.000", y: "8.000", selected: false },
4708+
{ label: "Point 5", rounded: false, x: "-14.000", y: "16.000", selected: false }
46874709
]);
46884710
await expect(page.locator("#statusLog")).toHaveValue(/OK Added copied point after point 2 for shape row 0\./);
46894711
await expect.poll(() => page.evaluate(() => window.__objectVectorStudioV2App.selectedShape().geometry.points.length)).toBe(5);
@@ -4699,10 +4721,10 @@ test.describe("Workspace Manager V2 bootstrap", () => {
46994721
y: row.querySelector("[data-polygon-point-axis='y']").value,
47004722
selected: row.dataset.polygonPointActionSelected === "true"
47014723
})))).toEqual([
4702-
{ label: "Point 1", rounded: false, x: "0", y: "-18", selected: false },
4703-
{ label: "Point 2", rounded: true, x: "14", y: "16", selected: false },
4704-
{ label: "Point 3", rounded: false, x: "0", y: "8", selected: false },
4705-
{ label: "Point 4", rounded: false, x: "-14", y: "16", selected: false }
4724+
{ label: "Point 1", rounded: false, x: "0.000", y: "-18.000", selected: false },
4725+
{ label: "Point 2", rounded: true, x: "14.000", y: "16.000", selected: false },
4726+
{ label: "Point 3", rounded: false, x: "0.000", y: "8.000", selected: false },
4727+
{ label: "Point 4", rounded: false, x: "-14.000", y: "16.000", selected: false }
47064728
]);
47074729
await expect.poll(() => page.locator("#objectVectorStudioV2ShapeGeometryDetails .object-vector-studio-v2__polygon-point-field").evaluateAll((rows) => rows.every((row) => row.dataset.polygonPointActionSelected !== "true"))).toBe(true);
47084730
await expect(page.locator("#statusLog")).toHaveValue(/OK Deleted point 3 from shape row 0\./);
@@ -5241,14 +5263,49 @@ test.describe("Workspace Manager V2 bootstrap", () => {
52415263
expect(lineAfterEndpoint.geometry.point2.y).not.toBe(lineBeforeEndpoint.geometry.point2.y);
52425264
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved line end for shape row 1\./);
52435265

5266+
const scaledLineStart = await page.evaluate(() => {
5267+
const app = window.__objectVectorStudioV2App;
5268+
const line = app.selectedObject().shapes[1];
5269+
line.geometry.point2 = { x: -10.4, y: 30.4 };
5270+
line.transform = { shapeOrigin: { x: 0, y: 0 }, rotation: 0, scaleX: 2, scaleY: 2, x: 0, y: 0 };
5271+
app.setSnapMode("grid", "Snap Grid");
5272+
app.selectShape(1, "scaled snap drag validation");
5273+
return app.transformedPoint(line.geometry.point2, line.transform);
5274+
});
5275+
await dragObjectVectorHandleToLogicalPoint(page, "#objectVectorStudioV2RenderSurface [data-line-endpoint='end']", scaledLineStart, { x: -15.2, y: 55.8 });
5276+
const scaledLineSnapDrag = await page.evaluate(() => {
5277+
const app = window.__objectVectorStudioV2App;
5278+
const line = app.selectedObject().shapes[1];
5279+
return {
5280+
geometryPoint: line.geometry.point2,
5281+
renderedPoint: app.transformedPoint(line.geometry.point2, line.transform)
5282+
};
5283+
});
5284+
expect(scaledLineSnapDrag).toEqual({
5285+
geometryPoint: { x: -7.5, y: 28 },
5286+
renderedPoint: { x: -15, y: 56 }
5287+
});
5288+
52445289
await page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='2']").click();
52455290
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-geometry-point-kind='polygon-point']")).toHaveCount(4);
5291+
await page.evaluate(() => {
5292+
const app = window.__objectVectorStudioV2App;
5293+
const polygon = app.selectedObject().shapes[2];
5294+
polygon.geometry.points[1] = { x: 42.4, y: 12.4 };
5295+
app.setSnapMode("grid", "Snap Grid");
5296+
app.selectShape(2, "off-grid point snap validation");
5297+
});
5298+
await dragObjectVectorHandleToLogicalPoint(page, "#objectVectorStudioV2RenderSurface [data-geometry-point-handle='polygon-1']", { x: 42.4, y: 12.4 }, { x: 47.2, y: 9.7 });
5299+
const polygonSnapDrag = await shapeSnapshot(2);
5300+
expect(polygonSnapDrag.geometry.points[1]).toEqual({ x: 47, y: 10 });
5301+
await expect(page.locator("#objectVectorStudioV2ShapeGeometryDetails [data-polygon-point-index='1'][data-polygon-point-axis='x']")).toHaveValue("47.000");
5302+
await expect(page.locator("#objectVectorStudioV2ShapeGeometryDetails [data-polygon-point-index='1'][data-polygon-point-axis='y']")).toHaveValue("10.000");
52465303
const polygonBeforePointDrag = await shapeSnapshot(2);
52475304
await dragLocator("#objectVectorStudioV2RenderSurface [data-geometry-point-handle='polygon-1']", 24, -16);
52485305
const polygonAfterPointDrag = await shapeSnapshot(2);
52495306
expect(polygonAfterPointDrag.geometry.points[1].x).not.toBe(polygonBeforePointDrag.geometry.points[1].x);
52505307
expect(polygonAfterPointDrag.geometry.points[1].y).not.toBe(polygonBeforePointDrag.geometry.points[1].y);
5251-
await expect(page.locator("#objectVectorStudioV2ShapeGeometryDetails [data-polygon-point-index='1'][data-polygon-point-axis='x']")).toHaveValue(String(polygonAfterPointDrag.geometry.points[1].x));
5308+
await expect(page.locator("#objectVectorStudioV2ShapeGeometryDetails [data-polygon-point-index='1'][data-polygon-point-axis='x']")).toHaveValue(polygonAfterPointDrag.geometry.points[1].x.toFixed(3));
52525309
await expect(page.locator("#statusLog")).toHaveValue(/OK Moved geometry point 2 for shape row 2\./);
52535310
const polygonBeforeBoundsDrag = await shapeSnapshot(2);
52545311
await dragLocator("#objectVectorStudioV2RenderSurface [data-resize-handle='se']", 18, 14);
@@ -10836,8 +10893,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
1083610893
await page.mouse.move(x + 18, y + 12, { steps: 4 });
1083710894
await expect.poll(async () => page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().geometry.points[0] }))).not.toEqual(pointBefore);
1083810895
const livePoint = await page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().geometry.points[0] }));
10839-
await expect(page.locator("#objectVectorStudioV2ShapeGeometryDetails [data-polygon-point-index='0'][data-polygon-point-axis='x']")).toHaveValue(String(livePoint.x));
10840-
await expect(page.locator("#objectVectorStudioV2ShapeGeometryDetails [data-polygon-point-index='0'][data-polygon-point-axis='y']")).toHaveValue(String(livePoint.y));
10896+
await expect(page.locator("#objectVectorStudioV2ShapeGeometryDetails [data-polygon-point-index='0'][data-polygon-point-axis='x']")).toHaveValue(livePoint.x.toFixed(3));
10897+
await expect(page.locator("#objectVectorStudioV2ShapeGeometryDetails [data-polygon-point-index='0'][data-polygon-point-axis='y']")).toHaveValue(livePoint.y.toFixed(3));
1084110898
await expect.poll(async () => page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='0']").getAttribute("points")).not.toBe(pointsBefore);
1084210899
await expect.poll(async () => {
1084310900
const box = await handle.boundingBox();

0 commit comments

Comments
 (0)