@@ -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 ( ) {
0 commit comments