Skip to content

Commit c39414d

Browse files
author
DavidQ
committed
Clean geometry defaults angle snap undo stroke caps and group icon color - PR_26133_074-geometry-defaults-angle-snap-undo-and-stroke-cap-fixes
1 parent 61f6521 commit c39414d

10 files changed

Lines changed: 202 additions & 147 deletions

File tree

docs/dev/reports/playwright_v8_coverage_report.md

Lines changed: 5 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_073-text-placement-edit-toolbar-and-right-click-paint-clear
3+
PR: PR_26133_074-geometry-defaults-angle-snap-undo-and-stroke-cap-fixes
44

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

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

2222
## Changed Runtime JS Files Covered
2323

24-
- (83%) tools/object-vector-studio-v2/js/bootstrap.js - executed lines 109/109; executed functions 5/6
25-
- (96%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 6594/6594; executed functions 669/700
24+
- (83%) tools/object-vector-studio-v2/js/bootstrap.js - executed lines 110/110; executed functions 5/6
25+
- (95%) tools/object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js - executed lines 452/452; executed functions 56/59
26+
- (96%) tools/object-vector-studio-v2/js/ToolStarterApp.js - executed lines 6645/6645; executed functions 673/704
2627

2728
## Changed JS Files Considered
2829

2930
- (83%) tools/object-vector-studio-v2/js/bootstrap.js - changed runtime JS file with browser V8 coverage
31+
- (95%) tools/object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js - changed runtime JS file with browser V8 coverage
3032
- (96%) tools/object-vector-studio-v2/js/ToolStarterApp.js - changed runtime JS file with browser V8 coverage
3133
- (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: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Playwright Workspace V2 Results
22

3-
PR: PR_26133_073-text-placement-edit-toolbar-and-right-click-paint-clear
3+
PR: PR_26133_074-geometry-defaults-angle-snap-undo-and-stroke-cap-fixes
44

55
## Validation
66

@@ -12,11 +12,12 @@ PR: PR_26133_073-text-placement-edit-toolbar-and-right-click-paint-clear
1212

1313
## Targeted Checks Covered
1414

15-
- Text tool uses first click, live preview, and second click commit for schema-valid text placement.
16-
- Text preview and committed text preserve active stroke color, stroke opacity, stroke width, and transparent fill.
17-
- Object Preview toolbar enables Undo, Copy, and Paste when valid; Paste uses copied shape data; Undo/Redo restore shape count.
18-
- Right-click on a shape applies transparent Paint/fill only to the clicked shape and suppresses the browser context menu.
19-
- Stroke-mode right-click no longer clears stroke.
15+
- Angle Snap help states that it applies to Object Transform Rotate and rounds the applied rotation delta to 15 degree increments.
16+
- Object Vector Studio V2 schema geometry definitions no longer carry pre-positioned defaults for click/move/click shape creation.
17+
- Stroke ending control is present in Palette and renders line/polyline/polygon/arc stroke caps and joins as round or square.
18+
- Object Preview pivot marker is smaller and labeled as the Origin/Pivot marker for rotation and scale.
19+
- Undo after a preview drag reverts the completed drag in one step rather than stepping through mousemove positions.
20+
- Polygon/Polyline Enter completion, Copy icon mapping, and Object Geometry group marker color remain covered by the workspace suite.
2021

2122
## Console/Runtime Errors
2223

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 80 additions & 19 deletions
Large diffs are not rendered by default.

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,13 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
185185
<span>Width</span>
186186
<input id="objectVectorStudioV2StrokeWidth" type="number" min="0.1" step="0.1" value="2">
187187
</label>
188+
<label class="object-vector-studio-v2__inline-field object-vector-studio-v2__stroke-cap-field" for="objectVectorStudioV2StrokeLinecap">
189+
<span>End</span>
190+
<select id="objectVectorStudioV2StrokeLinecap" title="Stroke endings and joins for lines, polylines, polygons, and arcs">
191+
<option value="round">Round</option>
192+
<option value="square">Square</option>
193+
</select>
194+
</label>
188195
</div>
189196
<div class="object-vector-studio-v2__palette-opacity-row">
190197
<span class="object-vector-studio-v2__palette-opacity-heading">Opacity</span>
@@ -269,7 +276,7 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
269276
<hr class="object-vector-studio-v2__separator">
270277
<div class="object-vector-studio-v2__snap-actions" aria-label="Snap controls">
271278
<button id="objectVectorStudioV2SnapModeButton" type="button" aria-pressed="true" title="Snap drawing and point dragging to grid intersections">Snap Grid</button>
272-
<button id="objectVectorStudioV2AngleSnapButton" type="button" aria-pressed="false" title="When enabled, the Rotate action rounds the entered rotation delta to 15 degree increments.">Angle Snap</button>
279+
<button id="objectVectorStudioV2AngleSnapButton" type="button" aria-pressed="false" title="Angle Snap is wired to Object Transform Rotate. Enable it before pressing Rotate to round the entered rotation delta to 15 degree increments.">Angle Snap</button>
273280
<button id="objectVectorStudioV2GridRenderButton" type="button" aria-pressed="false" title="Show or hide the preview grid">Grid</button>
274281
<button id="objectVectorStudioV2ToolLabelModeButton" type="button" aria-pressed="false" title="Toggle shape tool labels between icon-only and icon plus text">Icon/Text</button>
275282
</div>

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

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,10 @@ export class ToolStarterApp {
762762
}
763763
this.statusLog.write(`OK Palette stroke width set to ${width}.`);
764764
});
765+
this.elements.strokeLinecap.addEventListener("change", () => {
766+
this.elements.strokeLinecap.value = this.strokeLinecapValue();
767+
this.statusLog.write(`OK Palette stroke endings set to ${this.elements.strokeLinecap.value}.`);
768+
});
765769
[
766770
this.elements.fillOpacity,
767771
this.elements.strokeOpacity
@@ -847,6 +851,14 @@ export class ToolStarterApp {
847851
this.elements.strokeModeButton.dataset.paletteModeOpacity = String(this.selectedStrokeOpacity);
848852
}
849853

854+
strokeLinecapValue(value = this.elements.strokeLinecap?.value) {
855+
return value === "square" ? "square" : "round";
856+
}
857+
858+
strokeLinejoinValue(value = this.strokeLinecapValue()) {
859+
return value === "square" ? "miter" : "round";
860+
}
861+
850862
bindViewportControls() {
851863
this.elements.zoomInButton.addEventListener("click", () => this.zoomViewportByStep(ZOOM_STEP));
852864
this.elements.zoomOutButton.addEventListener("click", () => this.zoomViewportByStep(-ZOOM_STEP));
@@ -946,6 +958,8 @@ export class ToolStarterApp {
946958
this.applyIconGlyph(this.elements.snapModeButton, snapDetails.iconKey);
947959
this.elements.renderSurface.classList.toggle("is-snap-point-mode", this.snapMode === "point");
948960
this.elements.angleSnapButton.setAttribute("aria-pressed", String(this.angleSnapEnabled));
961+
this.elements.angleSnapButton.setAttribute("aria-label", "Angle Snap for Object Transform Rotate");
962+
this.elements.angleSnapButton.title = "Angle Snap is wired to Object Transform Rotate. Enable it before pressing Rotate to round the entered rotation delta to 15 degree increments.";
949963
this.elements.gridRenderButton.setAttribute("aria-pressed", String(this.gridRenderEnabled));
950964
this.elements.centerDotButton.setAttribute("aria-pressed", String(this.centerOriginVisible));
951965
this.elements.renderSurface.classList.toggle("is-grid-visible", this.gridRenderEnabled);
@@ -2762,8 +2776,8 @@ export class ToolStarterApp {
27622776
return;
27632777
}
27642778
element.style.strokeDasharray = this.drawingPreviewDashArray(previewShape.style.strokeWidth);
2765-
element.style.strokeLinecap = "round";
2766-
element.style.strokeLinejoin = "round";
2779+
element.style.strokeLinecap = this.strokeLinecapValue(previewShape.style.strokeLinecap);
2780+
element.style.strokeLinejoin = this.strokeLinejoinValue(previewShape.style.strokeLinecap);
27672781
}
27682782

27692783
drawingPreviewDashArray(strokeWidth) {
@@ -2911,11 +2925,17 @@ export class ToolStarterApp {
29112925
}
29122926

29132927
applySvgStyle(element, shape, { drawingScale = 1 } = {}) {
2928+
const geometryTool = shapeGeometryTool(shape);
29142929
element.setAttribute("fill", shape.style.fill);
29152930
element.setAttribute("stroke", shape.style.stroke);
29162931
element.setAttribute("stroke-width", shape.style.strokeWidth);
29172932
element.setAttribute("fill-opacity", shape.style.fillOpacity);
29182933
element.setAttribute("stroke-opacity", shape.style.strokeOpacity);
2934+
if (["line", "polyline", "polygon", "arc"].includes(geometryTool)) {
2935+
const lineCap = this.strokeLinecapValue(shape.style.strokeLinecap);
2936+
element.setAttribute("stroke-linecap", lineCap);
2937+
element.setAttribute("stroke-linejoin", this.strokeLinejoinValue(lineCap));
2938+
}
29192939
const transform = this.scaledDrawingTransform(this.shapeTransform(shape), drawingScale);
29202940
element.setAttribute("transform", this.svgTransformAttribute(transform));
29212941
}
@@ -3112,17 +3132,21 @@ export class ToolStarterApp {
31123132
const pivot = document.createElementNS(SVG_NS, "g");
31133133
pivot.classList.add("object-vector-studio-v2__pivot-origin");
31143134
pivot.dataset.pivotOrigin = String(this.selectedShapeIndex);
3135+
pivot.setAttribute("role", "img");
3136+
pivot.setAttribute("aria-label", "Origin/Pivot marker for selected shape rotation and scale");
3137+
const pivotTitle = document.createElementNS(SVG_NS, "title");
3138+
pivotTitle.textContent = "Origin/Pivot: rotate and scale pivot for the selected shape.";
31153139
const horizontal = document.createElementNS(SVG_NS, "line");
3116-
horizontal.setAttribute("x1", bounds.originX - 7);
3117-
horizontal.setAttribute("x2", bounds.originX + 7);
3140+
horizontal.setAttribute("x1", bounds.originX - 4);
3141+
horizontal.setAttribute("x2", bounds.originX + 4);
31183142
horizontal.setAttribute("y1", bounds.originY);
31193143
horizontal.setAttribute("y2", bounds.originY);
31203144
const vertical = document.createElementNS(SVG_NS, "line");
31213145
vertical.setAttribute("x1", bounds.originX);
31223146
vertical.setAttribute("x2", bounds.originX);
3123-
vertical.setAttribute("y1", bounds.originY - 7);
3124-
vertical.setAttribute("y2", bounds.originY + 7);
3125-
pivot.append(horizontal, vertical);
3147+
vertical.setAttribute("y1", bounds.originY - 4);
3148+
vertical.setAttribute("y2", bounds.originY + 4);
3149+
pivot.append(pivotTitle, horizontal, vertical);
31263150
this.elements.renderSurface.append(pivot);
31273151
} catch (error) {
31283152
this.statusLog.write(`FAIL Selection overlay render failed for ${object.name}/shape-${this.selectedShapeIndex} (${shapeTool(selectedShape)}): ${error.message}`);
@@ -3494,6 +3518,7 @@ export class ToolStarterApp {
34943518
fill: TRANSPARENT_STYLE_COLOR,
34953519
fillOpacity: this.selectedFillOpacity,
34963520
stroke: this.selectedStrokeColor || this.currentTargetColor() || "#ffffff",
3521+
strokeLinecap: this.strokeLinecapValue(),
34973522
strokeOpacity: this.selectedStrokeOpacity,
34983523
strokeWidth: Number.isFinite(strokeWidth) && strokeWidth > 0 ? strokeWidth : styleDefault.strokeWidth
34993524
};
@@ -3819,6 +3844,8 @@ export class ToolStarterApp {
38193844
event.preventDefault();
38203845
this.previewPointerEdit = {
38213846
mode: "move",
3847+
historyRecorded: false,
3848+
historySnapshot: this.cloneCurrentPayload(),
38223849
lastDelta: { x: 0, y: 0 },
38233850
originalGeometry: JSON.parse(JSON.stringify(selected.geometry)),
38243851
originalTransform: { ...this.shapeTransform(selected) },
@@ -3840,6 +3867,8 @@ export class ToolStarterApp {
38403867
event.stopPropagation();
38413868
this.previewPointerEdit = {
38423869
...options,
3870+
historyRecorded: false,
3871+
historySnapshot: this.cloneCurrentPayload(),
38433872
lastDelta: { x: 0, y: 0 },
38443873
originalGeometry: JSON.parse(JSON.stringify(selected.geometry)),
38453874
originalTransform: { ...this.shapeTransform(selected) },
@@ -3866,6 +3895,7 @@ export class ToolStarterApp {
38663895
if (Math.abs(delta.x - edit.lastDelta.x) < 0.001 && Math.abs(delta.y - edit.lastDelta.y) < 0.001) {
38673896
return;
38683897
}
3898+
this.recordPreviewPointerEditStart(edit);
38693899
edit.lastDelta = delta;
38703900
this.applyPreviewPointerEdit(edit, delta, { live: true });
38713901
}
@@ -3880,37 +3910,51 @@ export class ToolStarterApp {
38803910
if (Math.abs(delta.x) < 0.001 && Math.abs(delta.y) < 0.001) {
38813911
return;
38823912
}
3913+
this.recordPreviewPointerEditStart(edit);
38833914
this.applyPreviewPointerEdit(edit, delta, { live: false });
38843915
}
38853916

3917+
recordPreviewPointerEditStart(edit) {
3918+
if (edit.historyRecorded || !edit.historySnapshot) {
3919+
return;
3920+
}
3921+
this.previewUndoStack.push(this.clonePayloadValue(edit.historySnapshot));
3922+
if (this.previewUndoStack.length > PREVIEW_HISTORY_LIMIT) {
3923+
this.previewUndoStack.shift();
3924+
}
3925+
this.previewRedoStack = [];
3926+
edit.historyRecorded = true;
3927+
this.updatePreviewEditActionState();
3928+
}
3929+
38863930
applyPreviewPointerEdit(edit, delta, { live = false } = {}) {
38873931
if (edit.mode === "move") {
38883932
this.updateSelectedShapeTransform("preview move", (shape) => {
38893933
shape.transform = this.ensureShapeTransform(shape);
38903934
shape.transform.x = Number((edit.originalTransform.x + delta.x).toFixed(3));
38913935
shape.transform.y = Number((edit.originalTransform.y + delta.y).toFixed(3));
3892-
}, `OK ${live ? "Live " : ""}Dragged shape row ${edit.shapeIndex} by ${delta.x}, ${delta.y}.`);
3936+
}, `OK ${live ? "Live " : ""}Dragged shape row ${edit.shapeIndex} by ${delta.x}, ${delta.y}.`, { skipPreviewHistory: true });
38933937
return;
38943938
}
38953939

38963940
if (edit.mode === "line-endpoint") {
38973941
this.updateSelectedShapeGeometry("preview line endpoint", (shape) => {
38983942
shape.geometry = this.previewLineEndpointGeometry(shape, edit, delta);
3899-
}, `OK ${live ? "Live " : ""}Moved line ${edit.endpoint} for shape row ${edit.shapeIndex}.`);
3943+
}, `OK ${live ? "Live " : ""}Moved line ${edit.endpoint} for shape row ${edit.shapeIndex}.`, { skipPreviewHistory: true });
39003944
return;
39013945
}
39023946

39033947
if (edit.mode === "geometry-point") {
39043948
this.updateSelectedShapeGeometry("preview point handle", (shape) => {
39053949
shape.geometry = this.previewGeometryPointGeometry(shape, edit, delta);
3906-
}, `OK ${live ? "Live " : ""}Moved geometry point ${edit.control || edit.pointIndex + 1 || "handle"} for shape row ${edit.shapeIndex}.`);
3950+
}, `OK ${live ? "Live " : ""}Moved geometry point ${edit.control || edit.pointIndex + 1 || "handle"} for shape row ${edit.shapeIndex}.`, { skipPreviewHistory: true });
39073951
return;
39083952
}
39093953

39103954
if (edit.mode === "resize") {
39113955
this.updateSelectedShapeGeometry("preview resize", (shape) => {
39123956
shape.geometry = this.previewResizeGeometry(shape, edit, delta);
3913-
}, `OK ${live ? "Live " : ""}Resized shape row ${edit.shapeIndex} with ${edit.handle} handle.`);
3957+
}, `OK ${live ? "Live " : ""}Resized shape row ${edit.shapeIndex} with ${edit.handle} handle.`, { skipPreviewHistory: true });
39143958
}
39153959
}
39163960

@@ -4741,6 +4785,7 @@ export class ToolStarterApp {
47414785
fill: TRANSPARENT_STYLE_COLOR,
47424786
fillOpacity: styleOverride?.fillOpacity ?? this.selectedFillOpacity,
47434787
stroke: strokeColor,
4788+
strokeLinecap: styleOverride?.strokeLinecap ?? this.strokeLinecapValue(style.strokeLinecap),
47444789
strokeOpacity: styleOverride?.strokeOpacity ?? this.selectedStrokeOpacity,
47454790
strokeWidth: styleOverride?.strokeWidth ?? style.strokeWidth
47464791
};
@@ -4791,12 +4836,14 @@ export class ToolStarterApp {
47914836
if (!PRIMITIVE_TOOLS.includes(type)) {
47924837
throw new Error(`unsupported shape tool ${type}.`);
47934838
}
4839+
if (!geometry) {
4840+
throw new Error(`${shapeTypeLabel(type)} requires committed canvas placement geometry.`);
4841+
}
47944842

47954843
const base = this.schemaDefault("shapeCommon");
4796-
const geometryDefinition = type === "square" ? "rectangleGeometry" : `${type}Geometry`;
47974844
const shape = {
47984845
...base,
4799-
geometry: geometry ? JSON.parse(JSON.stringify(geometry)) : this.schemaDefault(geometryDefinition),
4846+
geometry: JSON.parse(JSON.stringify(geometry)),
48004847
order,
48014848
style: this.createShapeStyleDefault(type, color, styleOverride),
48024849
tool: type,
@@ -4919,6 +4966,9 @@ export class ToolStarterApp {
49194966
};
49204967
syncTarget("paint", effectiveShape.style?.fill);
49214968
syncTarget("stroke", effectiveShape.style?.stroke);
4969+
if (effectiveShape.style?.strokeLinecap) {
4970+
this.elements.strokeLinecap.value = this.strokeLinecapValue(effectiveShape.style.strokeLinecap);
4971+
}
49224972
if (changed && render) {
49234973
this.renderPalette();
49244974
}
@@ -5028,6 +5078,7 @@ export class ToolStarterApp {
50285078
const paletteLabel = swatch?.name || swatch?.id || swatch?.symbol || directLabel || label;
50295079
if (shouldApplyStroke) {
50305080
shape.style.stroke = color;
5081+
shape.style.strokeLinecap = this.strokeLinecapValue();
50315082
shape.style.strokeWidth = Number.isFinite(strokeWidth) && strokeWidth > 0 ? strokeWidth : 2;
50325083
shape.style.strokeOpacity = this.selectedStrokeOpacity;
50335084
} else {
@@ -5098,6 +5149,9 @@ export class ToolStarterApp {
50985149
if (Number.isFinite(style.strokeWidth) && style.strokeWidth > 0) {
50995150
this.elements.strokeWidth.value = String(style.strokeWidth);
51005151
}
5152+
if (style.strokeLinecap) {
5153+
this.elements.strokeLinecap.value = this.strokeLinecapValue(style.strokeLinecap);
5154+
}
51015155
this.updatePaletteModeSwatches();
51025156
if (this.runtimePalette) {
51035157
this.renderPalette();
@@ -6020,7 +6074,7 @@ export class ToolStarterApp {
60206074
};
60216075
}
60226076

6023-
updateSelectedShapeGeometry(operation, updater, okMessage) {
6077+
updateSelectedShapeGeometry(operation, updater, okMessage, options = {}) {
60246078
const selected = this.selectedShape();
60256079
if (!selected) {
60266080
this.statusLog.write(`WARN Geometry ${operation} skipped: no shape is selected.`);
@@ -6048,10 +6102,10 @@ export class ToolStarterApp {
60486102
this.statusLog.write(`FAIL Invalid geometry rejected for shape row ${this.selectedShapeIndex}: ${error.message}`);
60496103
return false;
60506104
}
6051-
return this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, okMessage, `Geometry ${operation} failed schema validation`);
6105+
return this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, okMessage, `Geometry ${operation} failed schema validation`, options);
60526106
}
60536107

6054-
updateSelectedShapeTransform(operation, updater, okMessage) {
6108+
updateSelectedShapeTransform(operation, updater, okMessage, options = {}) {
60556109
const selected = this.selectedShape();
60566110
if (!selected) {
60576111
this.statusLog.write(`WARN Transform ${operation} skipped: no shape is selected.`);
@@ -6084,7 +6138,7 @@ export class ToolStarterApp {
60846138
if (override) {
60856139
override.transform = this.ensureShapeTransform(shape);
60866140
}
6087-
this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, okMessage, `Transform ${operation} failed schema validation`);
6141+
this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, okMessage, `Transform ${operation} failed schema validation`, options);
60886142
}
60896143

60906144
duplicateSelectedShape() {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ window.addEventListener("DOMContentLoaded", () => {
9292
snapModeButton: requireElement("#objectVectorStudioV2SnapModeButton"),
9393
stopButton: requireElement("#objectVectorStudioV2StopButton"),
9494
strokeModeButton: requireElement("#objectVectorStudioV2StrokeModeButton"),
95+
strokeLinecap: requireElement("#objectVectorStudioV2StrokeLinecap"),
9596
strokeOpacity: requireElement("#objectVectorStudioV2StrokeOpacity"),
9697
strokeWidth: requireElement("#objectVectorStudioV2StrokeWidth"),
9798
tagFilter: requireElement("#objectVectorStudioV2TagFilter"),

0 commit comments

Comments
 (0)