Skip to content

Commit 78d824d

Browse files
author
DavidQ
committed
Support Select All rotation move Auto Center under Scale and update Copy icon - PR_26133_088-select-all-rotate-auto-center-placement-and-copy-icon
1 parent 107ab83 commit 78d824d

7 files changed

Lines changed: 212 additions & 33 deletions

File tree

docs/dev/reports/playwright_v8_coverage_report.md

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

3-
PR: PR_26133_087-grid-off-color-and-auto-center-balance
3+
PR: PR_26133_088-select-all-rotate-auto-center-placement-and-copy-icon
44

55
Command: `npm run test:workspace-v2`
66

@@ -9,8 +9,8 @@ Result: PASS
99
Coverage source: Playwright/Chromium built-in V8 coverage from the final passing workspace-v2 run.
1010

1111
Changed runtime JS coverage:
12-
- `tools/object-vector-studio-v2/js/bootstrap.js`: 83% entry coverage, 110/110 executed lines, 5/6 executed functions.
13-
- `tools/object-vector-studio-v2/js/ToolStarterApp.js`: 95% entry coverage, 7498/7498 executed lines, 751/789 executed functions.
12+
- `tools/object-vector-studio-v2/js/bootstrap.js`: 83% entry coverage, 109/109 executed lines, 5/6 executed functions.
13+
- `tools/object-vector-studio-v2/js/ToolStarterApp.js`: 95% entry coverage, 7574/7574 executed lines, 771/809 executed functions.
1414

1515
Notes:
1616
- The generated detailed text artifact remains at `docs/dev/reports/playwright_v8_coverage_report.txt`.
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
# Playwright Workspace V2 Results
22

3-
PR: PR_26133_087-grid-off-color-and-auto-center-balance
3+
PR: PR_26133_088-select-all-rotate-auto-center-placement-and-copy-icon
44

55
Command: `npm run test:workspace-v2`
66

77
Result: PASS
88

99
Summary:
1010
- 54/54 Playwright tests passed.
11-
- Final run completed in 6.1 minutes.
12-
- Object Vector Studio V2 Grid-off state now asserts that only the Grid icon uses the disabled red Snap color while button text stays default.
13-
- Auto Center coverage verifies the selected shape pivot moves to the visible object geometry bounds center, geometry JSON is unchanged, rendered bounds do not move, and workspace dirty state is set.
14-
- Existing Object Vector Studio V2 console/page error assertions remained clean in the covered flows.
11+
- Final run completed in 5.7 minutes.
12+
- Select All / explicit selected-set Rotate coverage verifies the full selected shape set rotates around the selected-set bounds center while preserving relative shape positions.
13+
- Single-shape Rotate and existing grouped-shape Rotate coverage remain in place and passed.
14+
- Auto Center now renders in Object Transform under Scale and continues to update the selected shape origin/pivot without changing geometry or moving visible bounds.
15+
- Copy now uses the `nf-fa-copy` Nerd Font glyph.
16+
- Existing console/page error assertions remained clean in the covered flows.
1517

1618
Manual/targeted verification notes:
17-
- Grid off icon color matches disabled Snap Angle icon color.
18-
- Auto Center uses visible object bounds, supports asymmetric geometry center balancing, and logs an OK status after successful pivot update.
19-
- Auto Center changes transform origin/pivot only; geometry points are not modified and visible bounds remain stable.
19+
- Object Transform layout shows Scale, then Auto Center, then the transform summary.
20+
- Auto Center logs OK after successful center/pivot balancing and marks the workspace dirty through the existing transform update path.
21+
- Selected-set rotation updates preview/bounds immediately after Rotate.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,7 +1575,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15751575
titles: {
15761576
add: title("#objectVectorStudioV2AddObjectButton"),
15771577
angle: title("#objectVectorStudioV2AngleSnapButton"),
1578-
autoCenter: title("#objectVectorStudioV2AutoCenterButton"),
15791578
grid: title("#objectVectorStudioV2GridRenderButton"),
15801579
polygon: title("[data-shape-tool='polygon']"),
15811580
polyline: title("[data-shape-tool='polyline']"),
@@ -1584,7 +1583,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15841583
zoomIn: title("#objectVectorStudioV2ZoomInButton")
15851584
},
15861585
viewportIcons: {
1587-
autoCenter: icon("#objectVectorStudioV2AutoCenterButton"),
15881586
down: icon("#objectVectorStudioV2PanDownButton"),
15891587
reset: icon("#objectVectorStudioV2ResetViewButton"),
15901588
up: icon("#objectVectorStudioV2PanUpButton"),
@@ -1633,7 +1631,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16331631
snap: { iconKey: "snapGrid", iconName: "nf-md-grid_large" }
16341632
});
16351633
expect(iconStyleState.previewEditIcons).toMatchObject({
1636-
copy: { iconKey: "copy", iconName: "nf-cod-copy" },
1634+
copy: { iconKey: "copy", iconName: "nf-fa-copy" },
16371635
paste: { iconKey: "paste", iconName: "nf-oct-paste" },
16381636
redo: { iconKey: "redo", iconName: "nf-md-redo" },
16391637
undo: { iconKey: "undo", iconName: "nf-md-undo" }
@@ -1694,7 +1692,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16941692
triangle: "none"
16951693
});
16961694
expect(Object.fromEntries(Object.entries(iconStyleState.viewportIcons).map(([key, value]) => [key, value.iconKey]))).toEqual({
1697-
autoCenter: "center",
16981695
down: "panDown",
16991696
reset: "reset",
17001697
up: "panUp",
@@ -1706,7 +1703,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17061703
expect(iconStyleState.titles).toEqual({
17071704
add: "Add a schema-valid object to the loaded payload",
17081705
angle: "Snap Angle switches Rotate to a constrained dropdown using the selected 15, 30, 45, or 90 degree step.",
1709-
autoCenter: "Disabled until a schema-valid object is selected.",
17101706
grid: "Show or hide the preview grid",
17111707
polygon: "Create a polygon shape on the selected object. Click to add points.\n\nDouble-click to finish.",
17121708
polyline: "Create a polyline shape on the selected object. Click to add points.\n\nDouble-click to finish.",
@@ -2137,7 +2133,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
21372133
await expect(page.locator("#objectVectorStudioV2RenderSurface")).toHaveAttribute("viewBox", "-1600 -1100 3200 2200");
21382134
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-center-origin='0,0']")).toHaveCount(1);
21392135
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-center-origin='0,0']")).toHaveAttribute("r", "9");
2140-
await expect(page.locator("#objectVectorStudioV2ViewportControls button")).toHaveText(["Out", "In", "Up", "Down", "Left", "Right", "View", "Center", "Auto Center"]);
2136+
await expect(page.locator("#objectVectorStudioV2ViewportControls button")).toHaveText(["Out", "In", "Up", "Down", "Left", "Right", "View", "Center"]);
21412137
await expect(page.locator("#objectVectorStudioV2CenterDotButton")).toHaveAttribute("aria-pressed", "true");
21422138
await page.locator("#objectVectorStudioV2PanRightButton").click();
21432139
await page.locator("#objectVectorStudioV2PanDownButton").click();
@@ -2192,22 +2188,37 @@ test.describe("Workspace Manager V2 bootstrap", () => {
21922188
await expect(page.locator("#objectVectorStudioV2ObjectTransform #objectVectorStudioV2MoveShapeButton")).toHaveCount(1);
21932189
const transformSummaryLayout = await page.locator("#objectVectorStudioV2ObjectTransform").evaluate((panel) => {
21942190
const scaleRow = panel.querySelector(".object-vector-studio-v2__scale-control-row");
2191+
const autoCenterRow = panel.querySelector(".object-vector-studio-v2__transform-control-row--auto-center");
2192+
const autoCenterButton = panel.querySelector("#objectVectorStudioV2AutoCenterButton");
21952193
const summary = panel.querySelector(".object-vector-studio-v2__transform-summary");
21962194
const summaryStyle = getComputedStyle(summary);
21972195
return {
2196+
autoCenterAfterScale: Boolean(scaleRow && autoCenterRow && (scaleRow.compareDocumentPosition(autoCenterRow) & Node.DOCUMENT_POSITION_FOLLOWING)),
2197+
autoCenterButtonText: autoCenterButton?.textContent.trim() || "",
2198+
autoCenterTitle: autoCenterButton?.title || "",
2199+
summaryAfterAutoCenter: Boolean(autoCenterRow && summary && (autoCenterRow.compareDocumentPosition(summary) & Node.DOCUMENT_POSITION_FOLLOWING)),
21982200
summaryAfterScale: Boolean(scaleRow && summary && (scaleRow.compareDocumentPosition(summary) & Node.DOCUMENT_POSITION_FOLLOWING)),
21992201
summaryCentered: summaryStyle.textAlign === "center",
22002202
summaryStartsWithoutTransform: !summary.textContent.trim().startsWith("Transform"),
2201-
summaryTopAtBottom: summary.getBoundingClientRect().top >= scaleRow.getBoundingClientRect().bottom
2203+
summaryTopAtBottom: summary.getBoundingClientRect().top >= autoCenterRow.getBoundingClientRect().bottom
22022204
};
22032205
});
22042206
expect(transformSummaryLayout).toEqual({
2207+
autoCenterAfterScale: true,
2208+
autoCenterButtonText: "Auto Center",
2209+
autoCenterTitle: "Balance Center",
2210+
summaryAfterAutoCenter: true,
22052211
summaryAfterScale: true,
22062212
summaryCentered: true,
22072213
summaryStartsWithoutTransform: true,
22082214
summaryTopAtBottom: true
22092215
});
22102216
const transformIconState = await page.locator("#objectVectorStudioV2ObjectTransform").evaluate((panel) => ({
2217+
autoCenter: {
2218+
iconKey: panel.querySelector("#objectVectorStudioV2AutoCenterButton").dataset.ovsIconKey,
2219+
iconName: panel.querySelector("#objectVectorStudioV2AutoCenterButton").dataset.ovsIconName,
2220+
title: panel.querySelector("#objectVectorStudioV2AutoCenterButton").title
2221+
},
22112222
resize: {
22122223
iconKey: panel.querySelector("#objectVectorStudioV2ResizeShapeButton").dataset.ovsIconKey,
22132224
iconName: panel.querySelector("#objectVectorStudioV2ResizeShapeButton").dataset.ovsIconName,
@@ -2216,6 +2227,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
22162227
scaleActionRemoved: panel.querySelector("#objectVectorStudioV2ScaleShapeButton") === null
22172228
}));
22182229
expect(transformIconState).toEqual({
2230+
autoCenter: { iconKey: "center", iconName: "nf-fa-dot_circle_o", title: "Balance Center" },
22192231
resize: { iconKey: "resize", iconName: "nf-md-resize", title: "Resize Geometry" },
22202232
scaleActionRemoved: true
22212233
});
@@ -2371,6 +2383,19 @@ test.describe("Workspace Manager V2 bootstrap", () => {
23712383
selectIds: ["objectVectorStudioV2RotateSnapSelect", "objectVectorStudioV2SnapAngleStepSelect"],
23722384
visibleInputIds: ["objectVectorStudioV2RotateInput"],
23732385
visibleSelectIds: []
2386+
},
2387+
{
2388+
allOneLine: true,
2389+
axisLabels: [],
2390+
buttonId: "objectVectorStudioV2AutoCenterButton",
2391+
buttonText: "Auto Center",
2392+
buttonTitle: "Balance Center",
2393+
inputIds: [],
2394+
label: "Center",
2395+
rowType: "auto-center",
2396+
selectIds: [],
2397+
visibleInputIds: [],
2398+
visibleSelectIds: []
23742399
}
23752400
]);
23762401
await expect(page.locator("#objectVectorStudioV2RotateInput")).toHaveAttribute("min", "-359");
@@ -5983,6 +6008,78 @@ test.describe("Workspace Manager V2 bootstrap", () => {
59836008
{ rotation: 0, x: 0, y: 0 }
59846009
]);
59856010

6011+
const selectedSetRotateBefore = await page.evaluate(() => {
6012+
const app = window.__objectVectorStudioV2App;
6013+
const indexes = app.selectedObject().shapes.map((shape, shapeIndex) => shapeIndex);
6014+
app.selectedShapeIndex = 0;
6015+
app.selectedShapeIndexes = new Set(indexes);
6016+
app.directSelectedShapeIndexes = new Set(indexes);
6017+
app.renderPayload();
6018+
const frame = app.activeFrame();
6019+
const bounds = app.shapeSetBounds(app.selectedObject(), indexes, { includeInvisible: false });
6020+
return {
6021+
bounds,
6022+
origins: indexes.map((shapeIndex) => {
6023+
const shape = app.effectiveShapeForFrame(app.selectedObject().shapes[shapeIndex], frame, shapeIndex);
6024+
const transform = app.ensureShapeTransform(shape);
6025+
return {
6026+
index: shapeIndex,
6027+
point: app.transformedPoint(transform.origin, transform),
6028+
rotation: transform.rotation
6029+
};
6030+
}),
6031+
pivot: {
6032+
x: Number((bounds.x + bounds.width / 2).toFixed(3)),
6033+
y: Number((bounds.y + bounds.height / 2).toFixed(3))
6034+
}
6035+
};
6036+
});
6037+
const selectionBoundsBeforeSelectedSetRotate = await page.locator("#objectVectorStudioV2RenderSurface [data-selection-bounds='0']").evaluate((box) => ({
6038+
height: Number(box.getAttribute("height")),
6039+
width: Number(box.getAttribute("width")),
6040+
x: Number(box.getAttribute("x")),
6041+
y: Number(box.getAttribute("y"))
6042+
}));
6043+
await page.locator("#objectVectorStudioV2RotateInput").fill("90");
6044+
await page.locator("#objectVectorStudioV2RotateShapeButton").click();
6045+
await expect(page.locator("#statusLog")).toHaveValue(/OK Rotated selected set \(4 shapes\) by 90 degrees\./);
6046+
const selectedSetRotateAfter = await page.evaluate(() => {
6047+
const app = window.__objectVectorStudioV2App;
6048+
const frame = app.activeFrame();
6049+
return app.selectedObject().shapes.map((shape, shapeIndex) => {
6050+
const effectiveShape = app.effectiveShapeForFrame(shape, frame, shapeIndex);
6051+
const transform = app.ensureShapeTransform(effectiveShape);
6052+
return {
6053+
index: shapeIndex,
6054+
point: app.transformedPoint(transform.origin, transform),
6055+
rotation: transform.rotation
6056+
};
6057+
});
6058+
});
6059+
const rotatePointAround = (point, pivot, degrees) => {
6060+
const radians = (degrees * Math.PI) / 180;
6061+
const relativeX = point.x - pivot.x;
6062+
const relativeY = point.y - pivot.y;
6063+
return {
6064+
x: pivot.x + relativeX * Math.cos(radians) - relativeY * Math.sin(radians),
6065+
y: pivot.y + relativeX * Math.sin(radians) + relativeY * Math.cos(radians)
6066+
};
6067+
};
6068+
selectedSetRotateBefore.origins.forEach((before) => {
6069+
const after = selectedSetRotateAfter.find((entry) => entry.index === before.index);
6070+
const expectedPoint = rotatePointAround(before.point, selectedSetRotateBefore.pivot, 90);
6071+
expect(after.point.x).toBeCloseTo(expectedPoint.x, 3);
6072+
expect(after.point.y).toBeCloseTo(expectedPoint.y, 3);
6073+
expect(after.rotation).toBe((before.rotation + 90) % 360);
6074+
});
6075+
const selectionBoundsAfterSelectedSetRotate = await page.locator("#objectVectorStudioV2RenderSurface [data-selection-bounds='0']").evaluate((box) => ({
6076+
height: Number(box.getAttribute("height")),
6077+
width: Number(box.getAttribute("width")),
6078+
x: Number(box.getAttribute("x")),
6079+
y: Number(box.getAttribute("y"))
6080+
}));
6081+
expect(selectionBoundsAfterSelectedSetRotate).not.toEqual(selectionBoundsBeforeSelectedSetRotate);
6082+
59866083
await page.evaluate(() => {
59876084
const app = window.__objectVectorStudioV2App;
59886085
app.selectedShapeIndex = 0;

tools/object-vector-studio-v2/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
121121
<button id="objectVectorStudioV2PanRightButton" type="button" title="Pan the work surface right">Right</button>
122122
<button id="objectVectorStudioV2ResetViewButton" type="button" title="Reset zoom and origin to 0,0">View</button>
123123
<button id="objectVectorStudioV2CenterDotButton" type="button" title="Show or hide the center dot">Center</button>
124-
<button id="objectVectorStudioV2AutoCenterButton" type="button" title="Balance selected shape origin/pivot to the visible object center">Auto Center</button>
125124
</div>
126125
<svg id="objectVectorStudioV2RenderSurface" class="object-vector-studio-v2__render-surface" viewBox="0 0 320 220" tabindex="0" role="img" aria-label="Object shape render surface"></svg>
127126
<div id="objectVectorStudioV2ObjectPreviewFooter" class="object-vector-studio-v2__preview-footer">Object ID: none</div>

0 commit comments

Comments
 (0)