Skip to content

Commit b54035f

Browse files
author
DavidQ
committed
Add eyedropper picker workflow and transparent right click paint stroke behavior - PR_26133_066-paint-stroke-picker-and-transparent-right-click
1 parent 1e2f91f commit b54035f

5 files changed

Lines changed: 212 additions & 57 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_065-object-preview-editing-selection-and-style-fixes
3+
PR: PR_26133_066-paint-stroke-picker-and-transparent-right-click
44

55
Source: docs/dev/reports/playwright_v8_coverage_report.txt generated by the latest npm run test:workspace-v2 run.
66

@@ -21,7 +21,7 @@ Source: docs/dev/reports/playwright_v8_coverage_report.txt generated by the late
2121

2222
## Changed Runtime JS Files Covered
2323

24-
- (95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 6304/6304; executed functions 639/671
24+
- (95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 6355/6355; executed functions 644/676
2525

2626
## Changed JS Files Considered
2727

@@ -30,4 +30,4 @@ Source: docs/dev/reports/playwright_v8_coverage_report.txt generated by the late
3030

3131
## Non-JS Changed Files
3232

33-
- tools/object-vector-studio-v2/styles/toolStarter.css - CSS layout/style change, not applicable to V8 coverage
33+
- tools/object-vector-studio-v2/index.html - markup change, not applicable to V8 coverage

docs/dev/reports/playwright_workspace_v2_results.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
# Playwright Workspace V2 Results
22

3-
PR: PR_26133_065-object-preview-editing-selection-and-style-fixes
3+
PR: PR_26133_066-paint-stroke-picker-and-transparent-right-click
44

55
## Validation
66

77
- PASS: npm run test:workspace-v2
88
- Result: 54 passed
9-
- Runtime: 4.3m
9+
- Runtime: 5.0m
1010
- Browser project: playwright
1111
- Workers: 1
1212

1313
## Targeted Checks Covered
1414

15-
- Empty canvas click deselects the current shape and clicking another shape selects it/show Object Geometry.
16-
- Escape no longer cancels Object Vector Studio V2 drawing mode; switching tools cancels without committing invalid geometry.
17-
- Object Geometry has no Apply Geometry button; input change events auto-apply valid geometry and visibly reject invalid geometry.
18-
- Shape body drag, point/handle drag, polygon/polyline editing, and circle selector-corner resize update preview geometry and dirty state.
19-
- Snap Grid drawing and handle movement snap to whole-number logical grid coordinates.
20-
- New drawn shapes preserve the selected stroke color after deselect.
21-
- Paint and Stroke mode application paths remain independent and do not mutate the opposite opacity.
15+
- Picker tool renders with the Nerd Font eye-dropper icon and samples fill/stroke colors, opacities, and stroke width into palette controls without recoloring shapes.
16+
- Paint and Stroke remain independent modes; selecting Shape/Tools activates Stroke mode and drawing keeps Stroke active.
17+
- Right-click inside Object Preview suppresses the browser context menu and applies transparent fill/stroke only to the clicked shape based on the active palette mode.
18+
- Fill and Stroke opacity values stay unchanged unless directly edited; applying Stroke does not mutate Fill opacity and applying Paint does not mutate Stroke opacity.
19+
- Selected circle resize handles render larger while existing non-circle selection chrome remains unchanged.
2220

2321
## Console/Runtime Errors
2422

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,7 +1423,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14231423
await page.locator("#objectVectorStudioV2ObjectNameInput").fill("Blocked Object");
14241424
await page.locator("#objectVectorStudioV2AddObjectButton").click();
14251425
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Add object blocked: load a schema-valid Object Vector Studio V2 payload before adding objects\./);
1426-
await expect(page.locator(".object-vector-studio-v2__tool-toggle")).toHaveText(["Select", "Arc", "Circle", "Ellipse", "Line", "Polygon", "Polyline", "Rectangle", "Square", "Triangle", "Text"]);
1426+
await expect(page.locator(".object-vector-studio-v2__tool-toggle")).toHaveText(["Select", "Arc", "Circle", "Ellipse", "Line", "Picker", "Polygon", "Polyline", "Rectangle", "Square", "Triangle", "Text"]);
14271427
const futureNotes = await readFile("tools/object-vector-studio-v2/possible.future.adds.txt", "utf8");
14281428
expect(futureNotes).toContain("Object Vector Studio V2 should stay focused on reusable atomic vector objects.");
14291429
expect(futureNotes).toContain("Future World Vector or Scene layers should instance Object Vector objects");
@@ -1511,6 +1511,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15111511
circle: icon(".object-vector-studio-v2__shape-icon--circle"),
15121512
ellipse: icon(".object-vector-studio-v2__shape-icon--ellipse"),
15131513
line: icon(".object-vector-studio-v2__shape-icon--line"),
1514+
picker: icon(".object-vector-studio-v2__shape-icon--picker"),
15141515
polygon: icon(".object-vector-studio-v2__shape-icon--polygon"),
15151516
polyline: icon(".object-vector-studio-v2__shape-icon--polyline"),
15161517
rectangle: icon(".object-vector-studio-v2__shape-icon--rectangle"),
@@ -1593,6 +1594,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15931594
circle: "circle",
15941595
ellipse: "ellipse",
15951596
line: "line",
1597+
picker: "picker",
15961598
polygon: "polygon",
15971599
polyline: "polyline",
15981600
rectangle: "rectangle",
@@ -1604,6 +1606,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16041606
expect(iconStyleState.shapeIcons.arc.iconName).toBe("nf-md-vector_radius");
16051607
expect(iconStyleState.shapeIcons.circle.iconName).toBe("nf-md-vector_circle_variant");
16061608
expect(iconStyleState.shapeIcons.ellipse.iconName).toBe("nf-md-vector_ellipse");
1609+
expect(iconStyleState.shapeIcons.picker.iconName).toBe("nf-fa-eye_dropper");
16071610
expect(iconStyleState.shapeIcons.polygon.iconName).toBe("nf-md-vector_polygon");
16081611
expect(iconStyleState.shapeIcons.polyline.iconName).toBe("nf-md-vector_polyline");
16091612
expect(iconStyleState.shapeIcons.square.iconName).toBe("nf-fa-vector_square");
@@ -1619,6 +1622,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16191622
circle: "none",
16201623
ellipse: "none",
16211624
line: "none",
1625+
picker: "none",
16221626
polygon: "none",
16231627
polyline: "none",
16241628
rectangle: "none",
@@ -1679,7 +1683,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16791683
};
16801684
});
16811685
expect(shapeToolLayout).toEqual({
1682-
buttonCount: 11,
1686+
buttonCount: 12,
16831687
labelBesideGrid: true,
16841688
leftPanelOverflowY: "auto",
16851689
textButtonWider: true,
@@ -2479,6 +2483,12 @@ test.describe("Workspace Manager V2 bootstrap", () => {
24792483
...eventInit
24802484
});
24812485
};
2486+
const rightClickPreviewShape = async (shapeIndex) => {
2487+
const locator = page.locator(`#objectVectorStudioV2RenderSurface [data-shape-index='${shapeIndex}']`);
2488+
const box = await locator.boundingBox();
2489+
expect(box).not.toBeNull();
2490+
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2, { button: "right" });
2491+
};
24822492

24832493
const leftPanelObjectScrollBefore = await forceLeftPanelScroll();
24842494
expect(leftPanelObjectScrollBefore.leftPanelScrollTop).toBeGreaterThan(0);
@@ -2889,6 +2899,93 @@ test.describe("Workspace Manager V2 bootstrap", () => {
28892899
await expect(page.locator("#objectVectorStudioV2JsonDetails")).toContainText('"strokeOpacity": 0.651');
28902900
await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='1']")).toHaveAttribute("stroke-opacity", "0.651");
28912901
await expect(page.locator("#statusLog")).toHaveValue(/OK Applied palette color #6fd3ff from cyan to shape row 1 by render surface click\. Target: stroke width 2, opacity 0\.651\./);
2902+
const shapeOneStyleAfterStrokeApply = await page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().style }));
2903+
expect(shapeOneStyleAfterStrokeApply.fillOpacity).toBe(0.502);
2904+
expect(shapeOneStyleAfterStrokeApply.strokeOpacity).toBe(0.651);
2905+
await expect(page.locator("#objectVectorStudioV2FillOpacity")).toHaveValue("128");
2906+
await expect(page.locator("#objectVectorStudioV2StrokeOpacity")).toHaveValue("166");
2907+
2908+
await page.evaluate(() => {
2909+
window.__objectVectorStudioV2ContextMenuPrevented = false;
2910+
document.querySelector("#objectVectorStudioV2RenderSurface").addEventListener("contextmenu", (event) => {
2911+
window.__objectVectorStudioV2ContextMenuPrevented = event.defaultPrevented;
2912+
}, { once: true });
2913+
});
2914+
await page.locator("#objectVectorStudioV2PaintModeButton").click();
2915+
await rightClickPreviewShape(1);
2916+
await expect(page.locator("#statusLog")).toHaveValue(/OK Applied transparent fill to shape row 1 by right-click\./);
2917+
await expect(page.locator("#objectVectorStudioV2JsonDetails")).toContainText('"fill": "#00000000"');
2918+
const shapeOneStyleAfterTransparentFill = await page.evaluate(() => ({
2919+
contextMenuPrevented: window.__objectVectorStudioV2ContextMenuPrevented,
2920+
style: { ...window.__objectVectorStudioV2App.selectedShape().style }
2921+
}));
2922+
expect(shapeOneStyleAfterTransparentFill).toEqual({
2923+
contextMenuPrevented: true,
2924+
style: {
2925+
...shapeOneStyleAfterStrokeApply,
2926+
fill: "#00000000"
2927+
}
2928+
});
2929+
await expect(page.locator("#objectVectorStudioV2FillOpacity")).toHaveValue("128");
2930+
await expect(page.locator("#objectVectorStudioV2StrokeOpacity")).toHaveValue("166");
2931+
2932+
await page.evaluate(() => {
2933+
window.__objectVectorStudioV2ContextMenuPrevented = false;
2934+
document.querySelector("#objectVectorStudioV2RenderSurface").addEventListener("contextmenu", (event) => {
2935+
window.__objectVectorStudioV2ContextMenuPrevented = event.defaultPrevented;
2936+
}, { once: true });
2937+
});
2938+
await page.locator("#objectVectorStudioV2StrokeModeButton").click();
2939+
await rightClickPreviewShape(1);
2940+
await expect(page.locator("#statusLog")).toHaveValue(/OK Applied transparent stroke to shape row 1 by right-click\./);
2941+
await expect(page.locator("#objectVectorStudioV2JsonDetails")).toContainText('"stroke": "#00000000"');
2942+
const shapeOneStyleAfterTransparentStroke = await page.evaluate(() => ({
2943+
contextMenuPrevented: window.__objectVectorStudioV2ContextMenuPrevented,
2944+
style: { ...window.__objectVectorStudioV2App.selectedShape().style }
2945+
}));
2946+
expect(shapeOneStyleAfterTransparentStroke).toEqual({
2947+
contextMenuPrevented: true,
2948+
style: {
2949+
...shapeOneStyleAfterTransparentFill.style,
2950+
stroke: "#00000000"
2951+
}
2952+
});
2953+
await expect(page.locator("#objectVectorStudioV2FillOpacity")).toHaveValue("128");
2954+
await expect(page.locator("#objectVectorStudioV2StrokeOpacity")).toHaveValue("166");
2955+
2956+
const shapeOneStyleBeforePicker = await page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().style }));
2957+
await page.keyboard.press("I");
2958+
await expect(page.locator('[data-shape-tool="picker"]')).toHaveAttribute("aria-pressed", "true");
2959+
await clickPreviewShape(1);
2960+
await expect(page.locator("#statusLog")).toHaveValue(/OK Picker sampled shape row 1: fill #00000000, stroke #00000000, fill opacity 0\.502, stroke opacity 0\.651, stroke width 2\./);
2961+
const pickerState = await page.evaluate(() => {
2962+
const app = window.__objectVectorStudioV2App;
2963+
return {
2964+
activeTool: app.activeTool,
2965+
fillInput: app.elements.fillOpacity.value,
2966+
paletteTarget: app.paletteTarget,
2967+
selectedFillColor: app.selectedFillColor,
2968+
selectedFillOpacity: app.selectedFillOpacity,
2969+
selectedStrokeColor: app.selectedStrokeColor,
2970+
selectedStrokeOpacity: app.selectedStrokeOpacity,
2971+
shapeStyle: { ...app.selectedShape().style },
2972+
strokeInput: app.elements.strokeOpacity.value,
2973+
strokeWidth: app.elements.strokeWidth.value
2974+
};
2975+
});
2976+
expect(pickerState).toEqual({
2977+
activeTool: "picker",
2978+
fillInput: "128",
2979+
paletteTarget: "stroke",
2980+
selectedFillColor: "#00000000",
2981+
selectedFillOpacity: 0.502,
2982+
selectedStrokeColor: "#00000000",
2983+
selectedStrokeOpacity: 0.651,
2984+
shapeStyle: shapeOneStyleBeforePicker,
2985+
strokeInput: "166",
2986+
strokeWidth: "2"
2987+
});
2988+
28922989
await page.evaluate(() => {
28932990
window.__objectVectorStudioV2App.selectedStrokeColor = "#123456";
28942991
window.__objectVectorStudioV2App.selectedStrokeLabel = "manual rogue";
@@ -2897,10 +2994,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
28972994
await clickPreviewShape(1);
28982995
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Palette color application rejected: #123456 is not in the loaded palette\./);
28992996
await expect(page.locator("#objectVectorStudioV2JsonDetails")).not.toContainText("#123456");
2900-
await page.locator("[data-palette-color='#ffffff']").click();
2901-
await page.keyboard.press("I");
2902-
await clickPreviewShape(1);
2903-
await expect(page.locator("#statusLog")).toHaveValue(/OK Sampled stroke color #6fd3ff from shape row 1\./);
29042997
await page.keyboard.press("X");
29052998
await expect(page.locator("#statusLog")).toHaveValue(/OK Swapped fill and stroke colors/);
29062999
await page.keyboard.press("D");
@@ -4098,6 +4191,11 @@ test.describe("Workspace Manager V2 bootstrap", () => {
40984191
expect(polygonAfterBoundsDrag.geometry.points[2].y).toBeGreaterThan(polygonBeforeBoundsDrag.geometry.points[2].y);
40994192
await expect(page.locator("#statusLog")).toHaveValue(/OK Resized shape row 2 with se handle\./);
41004193
await page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='3']").click();
4194+
const circleHandleSize = await page.locator("#objectVectorStudioV2RenderSurface [data-resize-handle='se']").evaluate((handle) => ({
4195+
height: Number(handle.getAttribute("height")),
4196+
width: Number(handle.getAttribute("width"))
4197+
}));
4198+
expect(circleHandleSize).toEqual({ height: 5, width: 5 });
41014199
const circleBeforeResize = await shapeSnapshot(3);
41024200
await dragLocator("#objectVectorStudioV2RenderSurface [data-resize-handle='se']", 28, 28);
41034201
const circleAfterResize = await shapeSnapshot(3);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
237237
<span class="object-vector-studio-v2__shape-icon object-vector-studio-v2__shape-icon--line" aria-hidden="true"></span>
238238
<span class="object-vector-studio-v2__tool-label">Line</span>
239239
</button>
240+
<button class="object-vector-studio-v2__tool-toggle" type="button" aria-pressed="false" data-shape-tool="picker" title="Sample a shape style into the palette controls">
241+
<span class="object-vector-studio-v2__shape-icon object-vector-studio-v2__shape-icon--picker" aria-hidden="true"></span>
242+
<span class="object-vector-studio-v2__tool-label">Picker</span>
243+
</button>
240244
<button class="object-vector-studio-v2__tool-toggle" type="button" aria-pressed="false" data-shape-tool="polygon" title="Create a polygon shape on the selected object">
241245
<span class="object-vector-studio-v2__shape-icon object-vector-studio-v2__shape-icon--polygon" aria-hidden="true"></span>
242246
<span class="object-vector-studio-v2__tool-label">Polygon</span>

0 commit comments

Comments
 (0)