Skip to content

Commit 191ae84

Browse files
author
DavidQ
committed
Polish Object Preview selection drag threshold and realtime bounds updates - PR_26133_097-shape-preview-selection-and-transform-polish
1 parent 20c5209 commit 191ae84

4 files changed

Lines changed: 163 additions & 33 deletions

File tree

docs/dev/reports/playwright_v8_coverage_report.md

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@ Exercised tool entry points detected:
2323
(0%) Workspace Manager - not exercised by this Playwright run
2424
2525
Changed runtime JS files covered:
26-
(63%) tools/asset-manager-v2/js/AssetManagerV2App.js - executed lines 643/643; executed functions 36/57
27-
(86%) tools/workspace-manager-v2/js/WorkspaceManagerV2App.js - executed lines 963/963; executed functions 42/49
28-
(92%) tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js - executed lines 1662/1662; executed functions 149/162
29-
(94%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 8286/8286; executed functions 809/860
30-
(100%) tools/storage-inspector-v2/js/services/StorageInspectorV2StorageService.js - executed lines 184/184; executed functions 29/29
26+
(94%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 8334/8334; executed functions 818/866
3127
3228
Files with executed line/function counts where available:
3329
(2%) src/engine/input/ActionInputService.js - executed lines 397/397; executed functions 1/51
@@ -216,7 +212,7 @@ Files with executed line/function counts where available:
216212
(94%) games/shared/workspaceGameMetadataHydrator.js - executed lines 106/106; executed functions 16/17
217213
(94%) src/engine/rendering/ObjectVectorRuntimeAssetService.js - executed lines 1136/1136; executed functions 111/118
218214
(94%) tools/common/PaletteSortService.js - executed lines 103/103; executed functions 17/18
219-
(94%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 8286/8286; executed functions 809/860
215+
(94%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 8334/8334; executed functions 818/866
220216
(95%) tools/object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js - executed lines 458/458; executed functions 56/59
221217
(98%) tools/storage-inspector-v2/js/StorageInspectorV2App.js - executed lines 347/347; executed functions 51/52
222218
(100%) games/Asteroids/flow/attract.js - executed lines 17/17; executed functions 1/1
@@ -300,14 +296,6 @@ Uncovered or low-coverage changed JS files:
300296
(100%) none - no low-coverage changed runtime JS files
301297
302298
Changed JS files considered:
303-
(0%) scripts/validate-json-contracts.mjs - changed JS file not collected as browser runtime coverage
304-
(0%) tests/games/AsteroidsAssetReferenceAdoption.test.mjs - changed JS file not collected as browser runtime coverage
305-
(0%) tests/games/AsteroidsPlatformDemo.test.mjs - changed JS file not collected as browser runtime coverage
306-
(0%) tests/playwright/tools/AssetManagerV2.spec.mjs - changed JS file not collected as browser runtime coverage
307299
(0%) tests/playwright/tools/WorkspaceManagerV2.spec.mjs - changed JS file not collected as browser runtime coverage
308-
(63%) tools/asset-manager-v2/js/AssetManagerV2App.js - changed JS file with browser V8 coverage
309-
(86%) tools/workspace-manager-v2/js/WorkspaceManagerV2App.js - changed JS file with browser V8 coverage
310-
(92%) tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js - changed JS file with browser V8 coverage
311300
(94%) tools/object-vector-studio-v2/js/ToolStarterApp.js - changed JS file with browser V8 coverage
312-
(100%) tools/storage-inspector-v2/js/services/StorageInspectorV2StorageService.js - changed JS file with browser V8 coverage
313301
```

docs/dev/reports/playwright_workspace_v2_results.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ Exit code: 0
88
99
Running 55 tests using 1 worker
1010
11-
Result: 55 passed (6.6m)
11+
Result: 55 passed (6.1m)
1212
13-
PR_26133_096 focused coverage included:
14-
- drags selected Object Vector Studio V2 shapes from preview selection bounds
15-
- multi-selected shapes move together from preview selection bounds
16-
- grouped shapes selected by group icon move together from preview selection bounds
17-
- outside-bounds drag follows normal deselect/no-move behavior
13+
Result for PR_26133_097:
14+
- PASS small pointer movement inside selected bounds does not start preview drag
15+
- PASS selected shape repeated click preserves the selected set
16+
- PASS empty canvas drag/click outside selected bounds follows normal deselect/no-move behavior
17+
- PASS single-shape live drag updates selection bounds and resize/point handle positions before mouseup
18+
- PASS multi-selected shapes and grouped shapes still move together from preview selection bounds
19+
- PASS realtime drag transform values remain max 3 decimals
1820
1921
Additional focused checks run before the full suite:
22+
- drags selected Object Vector Studio V2 shapes from preview selection bounds
2023
- Object Vector Studio V2 layout shell and schema-only palette gate
21-
- selection bounds alignment to transformed preview geometry
22-
- Object Vector Studio V2 asset authoring controls
23-
- dirty state through persisted edits and save outcomes
2424
```

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2832,7 +2832,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
28322832
await expect(page.locator("[data-palette-color='#ffffff']")).toHaveClass(/is-selected/);
28332833
await expect(page.locator("#statusLog")).toHaveValue(/Multi-select count: 2/);
28342834
await expect(page.locator("#objectVectorStudioV2JsonDetails")).toContainText('"selectedShapeIndexes"');
2835-
await clickPreviewShape(0);
2835+
await page.locator(".object-vector-studio-v2__object-tile.is-selected .object-vector-studio-v2__object-tile-shapes [data-object-tile-shape-index='0']").click();
28362836
await expect(page.locator("#objectVectorStudioV2StrokeModeButton")).toHaveAttribute("aria-pressed", "true");
28372837
await expect(page.locator("#objectVectorStudioV2PaintModeButton")).toHaveAttribute("aria-pressed", "false");
28382838
await expect.poll(async () => page.evaluate(() => window.__objectVectorStudioV2App.selectedShape().style.fill)).toBe("#6fd3ff");
@@ -6476,6 +6476,38 @@ test.describe("Workspace Manager V2 bootstrap", () => {
64766476
};
64776477
});
64786478
expect(selectedSetBefore.indexes).toEqual([1, 3]);
6479+
6480+
const tinyDragPoint = await objectVectorLogicalClientPoint(page, selectedSetBefore.bounds.x + selectedSetBefore.bounds.width / 2, selectedSetBefore.bounds.y + selectedSetBefore.bounds.height / 2);
6481+
await page.mouse.move(tinyDragPoint.x, tinyDragPoint.y);
6482+
await page.mouse.down();
6483+
await page.mouse.move(tinyDragPoint.x + 2, tinyDragPoint.y + 2);
6484+
await page.mouse.up();
6485+
const selectedSetAfterTinyDrag = await page.evaluate(() => {
6486+
const app = window.__objectVectorStudioV2App;
6487+
const frame = app.activeFrame();
6488+
const indexes = Array.from(app.selectedShapeIndexes).sort((left, right) => left - right);
6489+
return {
6490+
indexes,
6491+
transforms: indexes.map((shapeIndex) => {
6492+
const transform = app.effectiveShapeForFrame(app.selectedObject().shapes[shapeIndex], frame, shapeIndex).transform;
6493+
return { index: shapeIndex, x: transform.x, y: transform.y };
6494+
})
6495+
};
6496+
});
6497+
expect(selectedSetAfterTinyDrag.indexes).toEqual([1, 3]);
6498+
expect(selectedSetAfterTinyDrag.transforms).toEqual(selectedSetBefore.transforms);
6499+
6500+
await page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='1']").first().click();
6501+
const selectedSetAfterRepeatedClick = await page.evaluate(() => {
6502+
const app = window.__objectVectorStudioV2App;
6503+
return {
6504+
indexes: Array.from(app.selectedShapeIndexes).sort((left, right) => left - right),
6505+
selectedShapeIndex: app.selectedShapeIndex
6506+
};
6507+
});
6508+
expect(selectedSetAfterRepeatedClick.indexes).toEqual([1, 3]);
6509+
expect(selectedSetAfterRepeatedClick.selectedShapeIndex).toBe(1);
6510+
64796511
await mouseDragObjectVectorLogicalPoints(page, {
64806512
x: selectedSetBefore.bounds.x + selectedSetBefore.bounds.width / 2,
64816513
y: selectedSetBefore.bounds.y + selectedSetBefore.bounds.height / 2
@@ -6555,6 +6587,64 @@ test.describe("Workspace Manager V2 bootstrap", () => {
65556587
expect(outsideDragResult.selectedCount).toBe(0);
65566588
expect(outsideDragResult.selectedShapeIndex).toBe(-1);
65576589

6590+
await shapePanel.locator("[data-object-tile-shape-index='1']").click();
6591+
const liveDragStart = await page.evaluate(() => {
6592+
const app = window.__objectVectorStudioV2App;
6593+
const surface = document.querySelector("#objectVectorStudioV2RenderSurface");
6594+
const selectionBox = surface.querySelector("[data-selection-bounds='1']");
6595+
const handle = surface.querySelector("[data-resize-handle='nw']");
6596+
const effectiveShape = app.effectiveShape(app.selectedObject().shapes[1], 1);
6597+
const bounds = app.transformedBounds(effectiveShape);
6598+
const transform = effectiveShape.transform;
6599+
return {
6600+
bounds: {
6601+
height: Number(selectionBox.getAttribute("height")),
6602+
width: Number(selectionBox.getAttribute("width")),
6603+
x: Number(selectionBox.getAttribute("x")),
6604+
y: Number(selectionBox.getAttribute("y"))
6605+
},
6606+
handle: {
6607+
x: Number(handle.getAttribute("x")),
6608+
y: Number(handle.getAttribute("y"))
6609+
},
6610+
logicalCenter: {
6611+
x: bounds.x + bounds.width / 2,
6612+
y: bounds.y + bounds.height / 2
6613+
},
6614+
transform: { x: transform.x, y: transform.y }
6615+
};
6616+
});
6617+
const liveDragClientStart = await objectVectorLogicalClientPoint(page, liveDragStart.logicalCenter.x, liveDragStart.logicalCenter.y);
6618+
const liveDragClientEnd = await objectVectorLogicalClientPoint(page, liveDragStart.logicalCenter.x + 5.1234, liveDragStart.logicalCenter.y + 2.9876);
6619+
await page.mouse.move(liveDragClientStart.x, liveDragClientStart.y);
6620+
await page.mouse.down();
6621+
await page.mouse.move(liveDragClientEnd.x, liveDragClientEnd.y, { steps: 4 });
6622+
const liveDragDuring = await page.evaluate(() => {
6623+
const app = window.__objectVectorStudioV2App;
6624+
const surface = document.querySelector("#objectVectorStudioV2RenderSurface");
6625+
const selectionBox = surface.querySelector("[data-selection-bounds='1']");
6626+
const handle = surface.querySelector("[data-resize-handle='nw']");
6627+
const transform = app.effectiveShapeForFrame(app.selectedObject().shapes[1], app.activeFrame(), 1).transform;
6628+
return {
6629+
bounds: {
6630+
x: Number(selectionBox.getAttribute("x")),
6631+
y: Number(selectionBox.getAttribute("y"))
6632+
},
6633+
handle: {
6634+
x: Number(handle.getAttribute("x")),
6635+
y: Number(handle.getAttribute("y"))
6636+
},
6637+
transform: { x: transform.x, y: transform.y }
6638+
};
6639+
});
6640+
expect(liveDragDuring.bounds.x).not.toBe(liveDragStart.bounds.x);
6641+
expect(liveDragDuring.bounds.y).not.toBe(liveDragStart.bounds.y);
6642+
expect(liveDragDuring.handle.x).not.toBe(liveDragStart.handle.x);
6643+
expect(liveDragDuring.handle.y).not.toBe(liveDragStart.handle.y);
6644+
expect(String(liveDragDuring.transform.x).split(".")[1]?.length || 0).toBeLessThanOrEqual(3);
6645+
expect(String(liveDragDuring.transform.y).split(".")[1]?.length || 0).toBeLessThanOrEqual(3);
6646+
await page.mouse.up();
6647+
65586648
expect(pageErrors).toEqual([]);
65596649
} finally {
65606650
await coverageReporter.stop(page);

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

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const MIN_ZOOM = 0.01;
1818
const ZOOM_STEP = 0.01;
1919
const TRANSPARENT_STYLE_COLOR = "#00000000";
2020
const PREVIEW_HISTORY_LIMIT = 50;
21+
const PREVIEW_DRAG_START_THRESHOLD_PX = 5;
2122
const SNAP_MODES = Object.freeze(["grid", "point", "none"]);
2223
const ANGLE_SNAP_STEPS = Object.freeze([15, 30, 45, 90]);
2324
const POINT_SNAP_RADIUS = 1.5;
@@ -3325,9 +3326,31 @@ export class ToolStarterApp {
33253326
this.applySelectedPaletteColorToShape(shapeIndex, isStrokeMode ? "stroke" : "fill", options.source || (options.dragStart ? "render surface click" : "render surface drag"));
33263327
return;
33273328
}
3329+
if (this.preserveSelectedShapeClick(shapeIndex, options.source || "render surface")) {
3330+
return;
3331+
}
33283332
this.selectShape(shapeIndex, "render surface");
33293333
}
33303334

3335+
preserveSelectedShapeClick(shapeIndex, sourceLabel) {
3336+
const object = this.selectedObject();
3337+
const normalizedIndex = normalizeShapeIndex(shapeIndex);
3338+
const shapes = sortedShapes(object);
3339+
if (normalizedIndex < 0 || normalizedIndex >= shapes.length || !this.selectedShapeIndexes.has(normalizedIndex)) {
3340+
return false;
3341+
}
3342+
3343+
const scrollState = this.captureLeftPanelScrollState();
3344+
this.selectedShapeIndex = normalizedIndex;
3345+
this.syncPaletteSelectionFromCurrentShape({ logMissing: true });
3346+
this.setPaletteTarget("stroke", false);
3347+
this.renderPayload();
3348+
this.restoreLeftPanelScrollState(scrollState);
3349+
const shape = this.selectedShape();
3350+
this.statusLog.write(`OK Preserved selected shape from ${sourceLabel}: row ${this.selectedShapeIndex} (${shapeTool(shape)}). Multi-select count: ${this.selectedShapeIndexes.size}.`);
3351+
return true;
3352+
}
3353+
33313354
handleShapeContextMenu(event, shape, shapeIndex) {
33323355
event.preventDefault();
33333356
if (this.activeDrawing || this.previewPointerEdit) {
@@ -5012,12 +5035,10 @@ export class ToolStarterApp {
50125035
this.selectedShapeIndexes = new Set(targetIndexes);
50135036
this.directSelectedShapeIndexes = directSelectedShapeIndexes.size ? directSelectedShapeIndexes : new Set(targetIndexes);
50145037
this.previewPointerEdit = {
5038+
...this.previewPointerEditStartMetadata(event),
50155039
mode: "move-group",
50165040
groupId,
50175041
directSelectedShapeIndexes: new Set(this.directSelectedShapeIndexes),
5018-
historyRecorded: false,
5019-
historySnapshot: this.cloneCurrentPayload(),
5020-
lastDelta: { x: 0, y: 0 },
50215042
originalTransforms: Object.fromEntries(targetIndexes.map((targetIndex) => {
50225043
const effectiveShape = this.effectiveShapeForFrame(objectShapes[targetIndex], activeFrame, targetIndex);
50235044
return [targetIndex, this.ensureShapeTransform(effectiveShape)];
@@ -5032,10 +5053,8 @@ export class ToolStarterApp {
50325053
return;
50335054
}
50345055
this.previewPointerEdit = {
5056+
...this.previewPointerEditStartMetadata(event),
50355057
mode: "move",
5036-
historyRecorded: false,
5037-
historySnapshot: this.cloneCurrentPayload(),
5038-
lastDelta: { x: 0, y: 0 },
50395058
originalGeometry: JSON.parse(JSON.stringify(selected.geometry)),
50405059
originalTransform: { ...this.shapeTransform(selected) },
50415060
shapeIndex: normalizedIndex,
@@ -5055,17 +5074,44 @@ export class ToolStarterApp {
50555074
event.preventDefault();
50565075
event.stopPropagation();
50575076
this.previewPointerEdit = {
5077+
...this.previewPointerEditStartMetadata(event),
50585078
...options,
5059-
historyRecorded: false,
5060-
historySnapshot: this.cloneCurrentPayload(),
5061-
lastDelta: { x: 0, y: 0 },
50625079
originalGeometry: JSON.parse(JSON.stringify(selected.geometry)),
50635080
originalTransform: { ...this.shapeTransform(selected) },
50645081
shapeIndex: normalizedIndex,
50655082
start: this.snapCanvasPoint(this.pointerPreviewPoint(event), { excludeShapeIndex: normalizedIndex })
50665083
};
50675084
}
50685085

5086+
previewPointerEditStartMetadata(event) {
5087+
return {
5088+
dragThresholdMet: false,
5089+
historyRecorded: false,
5090+
historySnapshot: this.cloneCurrentPayload(),
5091+
lastDelta: { x: 0, y: 0 },
5092+
startClient: {
5093+
x: Number(event.clientX) || 0,
5094+
y: Number(event.clientY) || 0
5095+
}
5096+
};
5097+
}
5098+
5099+
previewPointerEditPastDragThreshold(edit, event) {
5100+
if (edit.dragThresholdMet) {
5101+
return true;
5102+
}
5103+
if (!edit.startClient) {
5104+
edit.dragThresholdMet = true;
5105+
return true;
5106+
}
5107+
const distance = Math.hypot((Number(event.clientX) || 0) - edit.startClient.x, (Number(event.clientY) || 0) - edit.startClient.y);
5108+
if (distance < PREVIEW_DRAG_START_THRESHOLD_PX) {
5109+
return false;
5110+
}
5111+
edit.dragThresholdMet = true;
5112+
return true;
5113+
}
5114+
50695115
previewPointerEditDelta(edit, event) {
50705116
const rawEnd = this.pointerPreviewPoint(event);
50715117
const end = this.snapCanvasPoint(rawEnd, { excludeShapeIndex: edit.shapeIndex });
@@ -5080,6 +5126,9 @@ export class ToolStarterApp {
50805126
if (!edit || event.buttons !== 1) {
50815127
return;
50825128
}
5129+
if (!this.previewPointerEditPastDragThreshold(edit, event)) {
5130+
return;
5131+
}
50835132
const delta = this.previewPointerEditDelta(edit, event);
50845133
if (Math.abs(delta.x - edit.lastDelta.x) < 0.001 && Math.abs(delta.y - edit.lastDelta.y) < 0.001) {
50855134
return;
@@ -5096,6 +5145,9 @@ export class ToolStarterApp {
50965145
return;
50975146
}
50985147
this.previewPointerEdit = null;
5148+
if (!edit.dragThresholdMet && !this.previewPointerEditPastDragThreshold(edit, event)) {
5149+
return;
5150+
}
50995151
const delta = this.previewPointerEditDelta(edit, event);
51005152
if (Math.abs(delta.x) < 0.001 && Math.abs(delta.y) < 0.001) {
51015153
return;

0 commit comments

Comments
 (0)