Skip to content

Commit 69d8a7a

Browse files
author
DavidQ
committed
Apply Snap Angle during drawing and limit free coordinates to three decimals - PR_26133_086-snap-angle-drawing-and-coordinate-formatting
1 parent 98fe9d4 commit 69d8a7a

5 files changed

Lines changed: 148 additions & 25 deletions

File tree

docs/dev/reports/playwright_v8_coverage_report.md

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

3-
PR: PR_26133_085-rounding-radius-rectangle-square-and-palette-picker-placement
3+
PR: PR_26133_086-snap-angle-drawing-and-coordinate-formatting
44

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

@@ -21,10 +21,9 @@ 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 7391/7391; executed functions 738/776
24+
- (95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 7440/7440; executed functions 743/781
2525

2626
## Changed JS Files Considered
2727

28-
- (95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - Object Vector Studio V2 runtime covered by browser V8 coverage while radius-based rectangle/square rounding, Snap Angle UI hiding, and Palette Picker sampling were exercised.
29-
- (0%) tools/object-vector-studio-v2/styles/toolStarter.css - CSS layout file, not collected as browser JS coverage.
28+
- (95%) tools/object-vector-studio-v2/js/ToolStarterApp.js - Object Vector Studio V2 runtime covered by browser V8 coverage while Snap None 3-decimal coordinate creation, Snap Angle drawing constraints, and Palette Picker placement were exercised.
3029
- (0%) tests/playwright/tools/WorkspaceManagerV2.spec.mjs - Playwright test file, not collected as browser runtime coverage.

docs/dev/reports/playwright_workspace_v2_results.md

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

3-
PR: PR_26133_085-rounding-radius-rectangle-square-and-palette-picker-placement
3+
PR: PR_26133_086-snap-angle-drawing-and-coordinate-formatting
44

55
## Validation
66

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

1313
## Targeted Checks Covered
1414

15-
- Rectangle point rounding now renders through a rounded SVG path and updates when Rounding Radius changes.
16-
- Square point rounding now renders through the same rounded rectangle path behavior while preserving equal width and height.
17-
- Invalid negative Rounding Radius values are visibly rejected and do not mutate the selected shape style.
18-
- Existing arc endpoint rounding and polygon/polyline/triangle point rounding coverage remained green.
19-
- Snap Angle disabled shows the Rotate numeric textbox while the dropdown and Step selector are hidden.
20-
- Snap Angle enabled hides the Rotate numeric textbox while showing the constrained dropdown and Step selector.
21-
- Palette primary row now contains Paint, Stroke, Width, and an icon-only Picker button to the right of Stroke controls.
22-
- Picker behavior still samples fill, stroke, opacities, and stroke width from a clicked shape without recoloring it.
15+
- Snap None shape creation stores geometry coordinates with no more than 3 decimal places.
16+
- Snap Angle enabled at 45 degrees constrains committed Line creation segments.
17+
- Snap Angle enabled at 45 degrees constrains committed Polyline creation segments from the prior point.
18+
- Snap Angle enabled at 45 degrees constrains committed Polygon creation segments from the prior point.
19+
- Snap Grid and Snap Point creation checks remain green.
20+
- Palette primary row order is Paint, Picker, Stroke, Width, with Picker kept icon-only.
2321

2422
## Console/Runtime Errors
2523

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2023,7 +2023,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
20232023
opacityLabels: Array.from(opacityRow.querySelectorAll("label > span")).map((label) => label.textContent.trim()),
20242024
opacityOrder: Array.from(opacityRow.children).map((element) => element.textContent.trim().replace(/\s+/g, " ")),
20252025
pickerIconOnly: content.querySelector("#objectVectorStudioV2PalettePickerButton").textContent.trim() === "",
2026-
pickerRightOfStrokeControls: pickerButton.left >= strokeButton.right && pickerButton.left >= widthLabel.right,
2026+
pickerBetweenPaintAndStroke: pickerButton.left >= paintButton.right && pickerButton.right <= strokeButton.left,
20272027
primaryInline: [strokeButton, widthLabel, pickerButton].every((rect) => Math.abs((paintButton.top + paintButton.height / 2) - (rect.top + rect.height / 2)) < 4),
20282028
primaryOrder: Array.from(primaryRow.children).map((element) => element.matches("label") ? element.querySelector("span").textContent.trim() : (element.getAttribute("aria-label") || element.textContent).trim()),
20292029
widthInputFitsXxDotX: Math.round(widthInput.width) >= 58,
@@ -2042,9 +2042,9 @@ test.describe("Workspace Manager V2 bootstrap", () => {
20422042
opacityLabels: ["Fill", "Stroke"],
20432043
opacityOrder: ["Opacity", "Fill", "Stroke"],
20442044
pickerIconOnly: true,
2045-
pickerRightOfStrokeControls: true,
2045+
pickerBetweenPaintAndStroke: true,
20462046
primaryInline: true,
2047-
primaryOrder: ["Paint", "Stroke", "Width", "Picker"],
2047+
primaryOrder: ["Paint", "Picker", "Stroke", "Width"],
20482048
widthInputFitsXxDotX: true,
20492049
widthIsRightOfStroke: true
20502050
});
@@ -3829,6 +3829,78 @@ test.describe("Workspace Manager V2 bootstrap", () => {
38293829
await drawObjectVectorShape(page, "line", [{ x: 5.25, y: 6.5 }, { x: 7.75, y: 8.25 }]);
38303830
const unsnappedLine = await page.evaluate(() => window.__objectVectorStudioV2App.selectedShape().geometry);
38313831
expect(unsnappedLine).toEqual({ point1: { x: 5.25, y: 6.5 }, point2: { x: 7.75, y: 8.25 } });
3832+
await drawObjectVectorShape(page, "line", [{ x: 5.1234, y: 6.5678 }, { x: 7.9876, y: 8.5432 }]);
3833+
const snapNoneFormattedLine = await page.evaluate(() => {
3834+
const geometry = window.__objectVectorStudioV2App.selectedShape().geometry;
3835+
const decimalLength = (value) => {
3836+
const [, decimals = ""] = String(value).split(".");
3837+
return decimals.length;
3838+
};
3839+
return {
3840+
geometry,
3841+
maxDecimals: Math.max(
3842+
decimalLength(geometry.point1.x),
3843+
decimalLength(geometry.point1.y),
3844+
decimalLength(geometry.point2.x),
3845+
decimalLength(geometry.point2.y)
3846+
)
3847+
};
3848+
});
3849+
expect(snapNoneFormattedLine.geometry.point1.x).toBeCloseTo(5.123, 3);
3850+
expect(snapNoneFormattedLine.geometry.point1.y).toBeCloseTo(6.568, 3);
3851+
expect(snapNoneFormattedLine.geometry.point2.x).toBeCloseTo(7.988, 3);
3852+
expect(snapNoneFormattedLine.geometry.point2.y).toBeCloseTo(8.543, 3);
3853+
expect(snapNoneFormattedLine.maxDecimals).toBeLessThanOrEqual(3);
3854+
3855+
await page.locator("#objectVectorStudioV2AngleSnapButton").click();
3856+
await expect(page.locator("#objectVectorStudioV2AngleSnapButton")).toHaveAttribute("aria-pressed", "true");
3857+
await page.locator("#objectVectorStudioV2SnapAngleStepSelect").selectOption("45");
3858+
await drawObjectVectorShape(page, "line", [{ x: 0, y: 0 }, { x: 10, y: 3 }]);
3859+
await drawObjectVectorShape(page, "polyline", [{ x: 20, y: 0 }, { x: 30, y: 3 }, { x: 38, y: 10 }]);
3860+
await drawObjectVectorShape(page, "polygon", [{ x: -30, y: 0 }, { x: -20, y: 3 }, { x: -12, y: 11 }, { x: -30, y: 14 }]);
3861+
const angleSnappedDrawing = await page.evaluate(() => {
3862+
const app = window.__objectVectorStudioV2App;
3863+
const shapeByTool = (tool) => [...app.selectedObject().shapes].reverse().find((shape) => shape.tool === tool);
3864+
const pointsForShape = (shape) => shape.tool === "line" ? [shape.geometry.point1, shape.geometry.point2] : shape.geometry.points;
3865+
const decimalLength = (value) => {
3866+
const [, decimals = ""] = String(value).split(".");
3867+
return decimals.length;
3868+
};
3869+
const segmentSnapsToStep = (start, end, step) => {
3870+
const dx = end.x - start.x;
3871+
const dy = end.y - start.y;
3872+
const degrees = ((Math.atan2(dy, dx) * 180 / Math.PI) % 360 + 360) % 360;
3873+
const nearest = Math.round(degrees / step) * step;
3874+
const wrappedDelta = Math.abs(((degrees - nearest + 540) % 360) - 180);
3875+
return wrappedDelta < 0.05;
3876+
};
3877+
const collect = (tool) => {
3878+
const shape = shapeByTool(tool);
3879+
const points = pointsForShape(shape);
3880+
const coordinates = points.flatMap((point) => [point.x, point.y]);
3881+
return {
3882+
geometry: shape.geometry,
3883+
maxDecimals: Math.max(...coordinates.map(decimalLength)),
3884+
segmentAnglesSnapped: points.slice(1).map((point, index) => segmentSnapsToStep(points[index], point, 45))
3885+
};
3886+
};
3887+
return {
3888+
angleStep: app.angleSnapStep,
3889+
line: collect("line"),
3890+
polygon: collect("polygon"),
3891+
polyline: collect("polyline")
3892+
};
3893+
});
3894+
expect(angleSnappedDrawing.angleStep).toBe(45);
3895+
expect(angleSnappedDrawing.line.segmentAnglesSnapped).toEqual([true]);
3896+
expect(angleSnappedDrawing.polyline.segmentAnglesSnapped).toEqual([true, true]);
3897+
expect(angleSnappedDrawing.polygon.segmentAnglesSnapped).toEqual([true, true, true]);
3898+
expect(angleSnappedDrawing.line.maxDecimals).toBeLessThanOrEqual(3);
3899+
expect(angleSnappedDrawing.polyline.maxDecimals).toBeLessThanOrEqual(3);
3900+
expect(angleSnappedDrawing.polygon.maxDecimals).toBeLessThanOrEqual(3);
3901+
await page.locator("#objectVectorStudioV2AngleSnapButton").click();
3902+
await expect(page.locator("#objectVectorStudioV2AngleSnapButton")).toHaveAttribute("aria-pressed", "false");
3903+
38323904
await page.locator("#objectVectorStudioV2SnapModeButton").click();
38333905
await expect(page.locator("#objectVectorStudioV2SnapModeButton")).toHaveText("Snap Grid");
38343906
await expect(page.locator("#objectVectorStudioV2SnapModeButton")).toHaveAttribute("data-snap-mode", "grid");

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
177177
<span class="object-vector-studio-v2__palette-mode-swatch" data-palette-mode-swatch="paint" aria-hidden="true"></span>
178178
<span class="object-vector-studio-v2__palette-mode-label">Paint</span>
179179
</button>
180+
<button id="objectVectorStudioV2PalettePickerButton" class="object-vector-studio-v2__palette-picker-button" type="button" aria-pressed="false" data-shape-tool="picker" title="Sample a shape style into Palette controls" aria-label="Picker">
181+
<span class="object-vector-studio-v2__shape-icon object-vector-studio-v2__shape-icon--picker" aria-hidden="true"></span>
182+
</button>
180183
<button id="objectVectorStudioV2StrokeModeButton" type="button" aria-pressed="false">
181184
<span class="object-vector-studio-v2__palette-mode-swatch" data-palette-mode-swatch="stroke" aria-hidden="true"></span>
182185
<span class="object-vector-studio-v2__palette-mode-label">Stroke</span>
@@ -185,9 +188,6 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
185188
<span>Width</span>
186189
<input id="objectVectorStudioV2StrokeWidth" type="number" min="0.1" step="0.1" value="2">
187190
</label>
188-
<button id="objectVectorStudioV2PalettePickerButton" class="object-vector-studio-v2__palette-picker-button" type="button" aria-pressed="false" data-shape-tool="picker" title="Sample a shape style into Palette controls" aria-label="Picker">
189-
<span class="object-vector-studio-v2__shape-icon object-vector-studio-v2__shape-icon--picker" aria-hidden="true"></span>
190-
</button>
191191
</div>
192192
<div class="object-vector-studio-v2__palette-opacity-row">
193193
<span class="object-vector-studio-v2__palette-opacity-heading">Opacity</span>

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

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3324,6 +3324,13 @@ export class ToolStarterApp {
33243324
return nearest?.point || null;
33253325
}
33263326

3327+
normalizeDrawingPoint(point) {
3328+
return {
3329+
x: this.formatViewportNumber(point?.x || 0),
3330+
y: this.formatViewportNumber(point?.y || 0)
3331+
};
3332+
}
3333+
33273334
snapCanvasPoint(point, options = {}) {
33283335
if (this.snapMode === "grid") {
33293336
return {
@@ -3340,7 +3347,54 @@ export class ToolStarterApp {
33403347
};
33413348
}
33423349
}
3343-
return point;
3350+
return this.normalizeDrawingPoint(point);
3351+
}
3352+
3353+
supportsDrawingAngleSnap(tool) {
3354+
return ["line", "polygon", "polyline"].includes(tool);
3355+
}
3356+
3357+
drawingAngleSnapAnchor(drawing) {
3358+
if (!drawing || !this.supportsDrawingAngleSnap(drawing.tool)) {
3359+
return null;
3360+
}
3361+
if (drawing.flow === "two-click") {
3362+
return drawing.start || null;
3363+
}
3364+
if (drawing.flow === "points") {
3365+
return drawing.points.at(-1) || null;
3366+
}
3367+
return null;
3368+
}
3369+
3370+
snapPointToDrawingAngle(anchor, point) {
3371+
const normalizedAnchor = this.normalizeDrawingPoint(anchor);
3372+
const normalizedPoint = this.normalizeDrawingPoint(point);
3373+
const dx = normalizedPoint.x - normalizedAnchor.x;
3374+
const dy = normalizedPoint.y - normalizedAnchor.y;
3375+
const distance = Math.hypot(dx, dy);
3376+
if (!distance) {
3377+
return normalizedPoint;
3378+
}
3379+
const degrees = Math.atan2(dy, dx) * 180 / Math.PI;
3380+
const snappedDegrees = this.snapAngle(degrees);
3381+
const radians = snappedDegrees * Math.PI / 180;
3382+
return {
3383+
x: this.formatViewportNumber(normalizedAnchor.x + Math.cos(radians) * distance),
3384+
y: this.formatViewportNumber(normalizedAnchor.y + Math.sin(radians) * distance)
3385+
};
3386+
}
3387+
3388+
snapDrawingPoint(point, drawing, options = {}) {
3389+
const snappedPoint = this.snapCanvasPoint(point, options);
3390+
if (!this.angleSnapEnabled) {
3391+
return this.normalizeDrawingPoint(snappedPoint);
3392+
}
3393+
const anchor = this.drawingAngleSnapAnchor(drawing);
3394+
if (!anchor) {
3395+
return this.normalizeDrawingPoint(snappedPoint);
3396+
}
3397+
return this.snapPointToDrawingAngle(anchor, snappedPoint);
33443398
}
33453399

33463400
createSvgShape(shape, { drawingScale = 1 } = {}) {
@@ -4290,7 +4344,7 @@ export class ToolStarterApp {
42904344
}
42914345
event.preventDefault();
42924346
event.stopPropagation();
4293-
const point = this.snapCanvasPoint(this.pointerPreviewPoint(event));
4347+
const point = this.snapDrawingPoint(this.pointerPreviewPoint(event), drawing);
42944348
this.drawingPreviewPoint = point;
42954349
this.drawingHintClientPoint = { x: event.clientX, y: event.clientY };
42964350
if (drawing.flow === "points") {
@@ -4328,7 +4382,7 @@ export class ToolStarterApp {
43284382
if (drawing.flow === "two-click" && !drawing.start) {
43294383
return;
43304384
}
4331-
const point = this.snapCanvasPoint(this.pointerPreviewPoint(event));
4385+
const point = this.snapDrawingPoint(this.pointerPreviewPoint(event), drawing);
43324386
if (drawing.preview && Math.abs(point.x - drawing.preview.x) < 0.001 && Math.abs(point.y - drawing.preview.y) < 0.001) {
43334387
return;
43344388
}

0 commit comments

Comments
 (0)