@@ -23,6 +23,7 @@ class TrimCommand extends Command {
2323 this . boundOnElementSelected = this . onElementSelected . bind ( this )
2424 this . boundOnLineClicked = this . onLineClicked . bind ( this )
2525 this . boundOnMouseMove = this . onMouseMove . bind ( this )
26+ this . boundOnRightClick = this . onRightClick . bind ( this )
2627 this . cleanup = this . cleanup . bind ( this )
2728 this . isTrimming = false
2829 this . ghostLine = null
@@ -39,6 +40,7 @@ class TrimCommand extends Command {
3940 this . editor . signals . terminalLogged . dispatch ( { type : 'strong' , msg : this . name . toUpperCase ( ) + ' ' } )
4041 this . editor . signals . commandCancelled . addOnce ( this . cleanup , this )
4142 document . addEventListener ( 'keydown' , this . boundOnKeyDown )
43+ document . addEventListener ( 'contextmenu' , this . boundOnRightClick , true )
4244
4345 if ( this . editor . selected . length > 0 ) {
4446 this . boundaryElements = [ ...this . editor . selected ]
@@ -60,7 +62,9 @@ class TrimCommand extends Command {
6062 }
6163
6264 onKeyDown ( event ) {
63- if ( event . key === 'Enter' || event . key === ' ' ) {
65+ if ( event . code === 'Enter' || event . code === 'Space' || event . code === 'NumpadEnter' ) {
66+ event . preventDefault ( )
67+ event . stopPropagation ( )
6468 if ( ! this . isTrimming ) {
6569 if ( this . boundaryElements . length === 0 ) {
6670 this . autoTrimMode = true
@@ -76,12 +80,35 @@ class TrimCommand extends Command {
7680
7781 this . startTrimmingLines ( )
7882 this . editor . signals . requestHoverCheck . dispatch ( )
83+ } else {
84+ this . finishCommand ( )
7985 }
8086 } else if ( event . key === 'Escape' ) {
8187 this . cleanup ( )
8288 }
8389 }
8490
91+ finishCommand ( ) {
92+ this . cleanup ( )
93+ this . editor . isInteracting = false
94+ this . editor . isDrawing = false
95+ this . editor . isSelecting = false
96+ this . editor . isTypingText = false
97+ this . editor . selectSingleElement = false
98+ this . editor . signals . commandCancelled . dispatch ( )
99+ this . editor . signals . terminalLogged . dispatch ( { msg : 'Command finished.' } )
100+ setTimeout ( ( ) => {
101+ const terminalInput = document . getElementById ( 'terminalInput' )
102+ if ( terminalInput ) terminalInput . focus ( )
103+ } , 0 )
104+ }
105+
106+ onRightClick ( event ) {
107+ event . preventDefault ( )
108+ event . stopPropagation ( )
109+ this . finishCommand ( )
110+ }
111+
85112 onElementSelected ( el ) {
86113 if ( ! this . isTrimming ) {
87114 const index = this . boundaryElements . findIndex ( b => b . node === el . node )
@@ -163,6 +190,34 @@ class TrimCommand extends Command {
163190
164191 const candidateBoundaries = this . getCandidateBoundaries ( originalEl )
165192
193+ const getArcBoundaryData = ( boundary ) => {
194+ const circleTrimData = boundary . data ( 'circleTrimData' )
195+ if ( circleTrimData ) {
196+ return {
197+ cx : circleTrimData . cx ,
198+ cy : circleTrimData . cy ,
199+ r : circleTrimData . r ,
200+ startAngle : circleTrimData . theta2 ,
201+ endAngle : circleTrimData . theta1 ,
202+ ccw : circleTrimData . ccw !== undefined ? circleTrimData . ccw : true ,
203+ }
204+ }
205+
206+ const arcData = boundary . data ( 'arcData' )
207+ if ( ! arcData ) return null
208+ const arcGeo = this . getArcGeometry ( arcData )
209+ if ( ! arcGeo ) return null
210+
211+ return {
212+ cx : arcGeo . cx ,
213+ cy : arcGeo . cy ,
214+ r : arcGeo . r ,
215+ startAngle : arcGeo . theta2 ,
216+ endAngle : arcGeo . theta1 ,
217+ ccw : arcGeo . ccw ,
218+ }
219+ }
220+
166221 const checkAndAddIntersection = ( intersect ) => {
167222 if ( ! intersect ) return
168223 const minX = Math . min ( lineEq . x1 , lineEq . x2 ) - 1e-4
@@ -204,6 +259,18 @@ class TrimCommand extends Command {
204259 } else if ( boundary . type === 'rect' ) {
205260 const rectBounds = { x : boundary . x ( ) , y : boundary . y ( ) , width : boundary . width ( ) , height : boundary . height ( ) }
206261 getLineRectIntersections ( { x1 : lineEq . x1 , y1 : lineEq . y1 , x2 : lineEq . x2 , y2 : lineEq . y2 } , rectBounds ) . forEach ( checkAndAddIntersection )
262+ } else if ( boundary . type === 'path' && ( boundary . data ( 'circleTrimData' ) || boundary . data ( 'arcData' ) ) ) {
263+ const arcBoundary = getArcBoundaryData ( boundary )
264+ if ( arcBoundary ) {
265+ getLineCircleIntersections (
266+ { x1 : lineEq . x1 , y1 : lineEq . y1 , x2 : lineEq . x2 , y2 : lineEq . y2 } ,
267+ { cx : arcBoundary . cx , cy : arcBoundary . cy , r : arcBoundary . r }
268+ ) . forEach ( pt => {
269+ if ( isPointInArc ( pt , arcBoundary . cx , arcBoundary . cy , arcBoundary . startAngle , arcBoundary . endAngle , arcBoundary . ccw ) ) {
270+ checkAndAddIntersection ( pt )
271+ }
272+ } )
273+ }
207274 } else if ( boundary . type === 'path' ) {
208275 getPathIntersections ( el , boundary ) . forEach ( checkAndAddIntersection )
209276 } else if ( boundary . type === 'polyline' ) {
@@ -338,6 +405,8 @@ class TrimCommand extends Command {
338405
339406 const candidateBoundaries = this . getCandidateBoundaries ( el )
340407 const intersections = [ ]
408+ const segmentTolerance = Math . max ( 1e-4 , r * 1e-6 )
409+ const tangentTolerance = Math . max ( segmentTolerance * 5 , r * 1e-5 )
341410
342411 const checkPointOnArc = ( pt ) => {
343412 if ( ! isArc ) return true
@@ -354,6 +423,54 @@ class TrimCommand extends Command {
354423 intersections . push ( { theta, x : intersect . x , y : intersect . y } )
355424 }
356425
426+ const isPointOnSegment = ( pt , seg , tolerance = segmentTolerance ) => {
427+ const dx = seg . x2 - seg . x1
428+ const dy = seg . y2 - seg . y1
429+ const len2 = dx * dx + dy * dy
430+ if ( len2 < 1e-12 ) return Math . hypot ( pt . x - seg . x1 , pt . y - seg . y1 ) <= tolerance
431+
432+ const t = ( ( pt . x - seg . x1 ) * dx + ( pt . y - seg . y1 ) * dy ) / len2
433+ const paramTolerance = Math . max ( 1e-4 , tolerance / Math . sqrt ( len2 ) )
434+ if ( t < - paramTolerance || t > 1 + paramTolerance ) return false
435+
436+ const clampedT = Math . max ( 0 , Math . min ( 1 , t ) )
437+ const closestX = seg . x1 + clampedT * dx
438+ const closestY = seg . y1 + clampedT * dy
439+ return Math . hypot ( pt . x - closestX , pt . y - closestY ) <= tolerance
440+ }
441+
442+ const checkLineCircleBoundary = ( seg ) => {
443+ const hits = getLineCircleIntersections ( seg , { cx, cy, r } )
444+ let accepted = 0
445+ hits . forEach ( pt => {
446+ if ( isPointOnSegment ( pt , seg , tangentTolerance ) ) {
447+ accepted ++
448+ checkAndAddIntersection ( pt )
449+ }
450+ } )
451+
452+ if ( accepted > 0 ) return
453+
454+ const dx = seg . x2 - seg . x1
455+ const dy = seg . y2 - seg . y1
456+ const len2 = dx * dx + dy * dy
457+ if ( len2 < 1e-12 ) return
458+
459+ const tRaw = ( ( cx - seg . x1 ) * dx + ( cy - seg . y1 ) * dy ) / len2
460+ const paramTolerance = Math . max ( 1e-4 , tangentTolerance / Math . sqrt ( len2 ) )
461+ if ( tRaw < - paramTolerance || tRaw > 1 + paramTolerance ) return
462+
463+ const t = Math . max ( 0 , Math . min ( 1 , tRaw ) )
464+ const closest = { x : seg . x1 + t * dx , y : seg . y1 + t * dy }
465+ const dist = Math . hypot ( closest . x - cx , closest . y - cy )
466+ if ( Math . abs ( dist - r ) > tangentTolerance || dist < 1e-12 ) return
467+
468+ checkAndAddIntersection ( {
469+ x : cx + ( ( closest . x - cx ) / dist ) * r ,
470+ y : cy + ( ( closest . y - cy ) / dist ) * r ,
471+ } )
472+ }
473+
357474 // Add endpoints if it's an arc
358475 if ( isArc ) {
359476 intersections . push ( { theta : arcGeo . theta2 , x : arcGeo . startPt ? arcGeo . startPt . x : ( cx + r * Math . cos ( arcGeo . theta2 ) ) , y : arcGeo . startPt ? arcGeo . startPt . y : ( cy + r * Math . sin ( arcGeo . theta2 ) ) } )
@@ -365,15 +482,7 @@ class TrimCommand extends Command {
365482
366483 if ( boundary . type === 'line' ) {
367484 const bEq = getLineEquation ( boundary )
368- getLineCircleIntersections ( bEq , { cx, cy, r } ) . forEach ( pt => {
369- const intersectMinX = Math . min ( bEq . x1 , bEq . x2 ) - 1e-4 ;
370- const intersectMaxX = Math . max ( bEq . x1 , bEq . x2 ) + 1e-4 ;
371- const intersectMinY = Math . min ( bEq . y1 , bEq . y2 ) - 1e-4 ;
372- const intersectMaxY = Math . max ( bEq . y1 , bEq . y2 ) + 1e-4 ;
373- if ( pt . x >= intersectMinX && pt . x <= intersectMaxX && pt . y >= intersectMinY && pt . y <= intersectMaxY ) {
374- checkAndAddIntersection ( pt )
375- }
376- } )
485+ checkLineCircleBoundary ( bEq )
377486 } else if ( boundary . type === 'rect' ) {
378487 const rectBounds = { x : boundary . x ( ) , y : boundary . y ( ) , width : boundary . width ( ) , height : boundary . height ( ) }
379488 const h = rectBounds . height ;
@@ -385,15 +494,7 @@ class TrimCommand extends Command {
385494 { x1 : rectBounds . x , y1 : rectBounds . y + h , x2 : rectBounds . x , y2 : rectBounds . y }
386495 ]
387496 rectSegments . forEach ( seg => {
388- getLineCircleIntersections ( seg , { cx, cy, r } ) . forEach ( pt => {
389- const intersectMinX = Math . min ( seg . x1 , seg . x2 ) - 1e-4 ;
390- const intersectMaxX = Math . max ( seg . x1 , seg . x2 ) + 1e-4 ;
391- const intersectMinY = Math . min ( seg . y1 , seg . y2 ) - 1e-4 ;
392- const intersectMaxY = Math . max ( seg . y1 , seg . y2 ) + 1e-4 ;
393- if ( pt . x >= intersectMinX && pt . x <= intersectMaxX && pt . y >= intersectMinY && pt . y <= intersectMaxY ) {
394- checkAndAddIntersection ( pt )
395- }
396- } )
497+ checkLineCircleBoundary ( seg )
397498 } )
398499 } else if ( boundary . type === 'path' && ( boundary . data ( 'circleTrimData' ) || boundary . data ( 'arcData' ) ) ) {
399500 const bArcData = boundary . data ( 'circleTrimData' ) || this . getArcGeometry ( boundary . data ( 'arcData' ) )
@@ -406,15 +507,7 @@ class TrimCommand extends Command {
406507 getPathIntersections ( boundary , el ) . forEach ( checkAndAddIntersection )
407508 } else if ( boundary . type === 'polyline' ) {
408509 getPolylineSegments ( boundary ) . forEach ( seg => {
409- getLineCircleIntersections ( seg , { cx, cy, r } ) . forEach ( pt => {
410- const minX = Math . min ( seg . x1 , seg . x2 ) - 1e-4
411- const maxX = Math . max ( seg . x1 , seg . x2 ) + 1e-4
412- const minY = Math . min ( seg . y1 , seg . y2 ) - 1e-4
413- const maxY = Math . max ( seg . y1 , seg . y2 ) + 1e-4
414- if ( pt . x >= minX && pt . x <= maxX && pt . y >= minY && pt . y <= maxY ) {
415- checkAndAddIntersection ( pt )
416- }
417- } )
510+ checkLineCircleBoundary ( seg )
418511 } )
419512 } else if ( boundary . type === 'circle' ) {
420513 const bcx = boundary . cx ( ) , bcy = boundary . cy ( ) , br = parseFloat ( boundary . radius ? boundary . radius ( ) : ( boundary . attr ( 'r' ) || boundary . attr ( 'rx' ) ) )
@@ -430,13 +523,7 @@ class TrimCommand extends Command {
430523 x1 : ecx + erx * Math . cos ( a1 ) , y1 : ecy + ery * Math . sin ( a1 ) ,
431524 x2 : ecx + erx * Math . cos ( a2 ) , y2 : ecy + ery * Math . sin ( a2 ) ,
432525 }
433- getLineCircleIntersections ( seg , { cx, cy, r } ) . forEach ( pt => {
434- const minX = Math . min ( seg . x1 , seg . x2 ) - 1e-4
435- const maxX = Math . max ( seg . x1 , seg . x2 ) + 1e-4
436- const minY = Math . min ( seg . y1 , seg . y2 ) - 1e-4
437- const maxY = Math . max ( seg . y1 , seg . y2 ) + 1e-4
438- if ( pt . x >= minX && pt . x <= maxX && pt . y >= minY && pt . y <= maxY ) checkAndAddIntersection ( pt )
439- } )
526+ checkLineCircleBoundary ( seg )
440527 }
441528 }
442529
@@ -1014,7 +1101,10 @@ class TrimCommand extends Command {
10141101 if ( ! this . isTrimming || ! this . editor . isInteracting ) return
10151102
10161103 const hoveredList = this . editor . hoveredElements || [ ]
1017- let targetEl = null
1104+ const activeSvg = this . editor . mode === 'paper' ? this . editor . paperSvg : this . editor . svg
1105+ if ( ! activeSvg ) return
1106+ const pt = activeSvg . point ( e . pageX , e . pageY )
1107+ let trimData = null
10181108
10191109 for ( const item of hoveredList ) {
10201110 const el = window . SVG ( item . node )
@@ -1024,20 +1114,17 @@ class TrimCommand extends Command {
10241114 if ( el . type === 'path' && ( el . data ( 'circleTrimData' ) || el . data ( 'arcData' ) || el . data ( 'splineData' ) ) ) isValidHover = true
10251115
10261116 if ( isValidHover ) {
1027- targetEl = el
1028- break
1117+ trimData = this . calculateTrim ( el , pt )
1118+ if ( trimData && trimData . preview ) break
10291119 }
10301120 }
10311121
1032- if ( ! targetEl ) {
1122+ if ( ! trimData || ! trimData . preview ) {
10331123 this . clearGhost ( )
10341124 return
10351125 }
10361126
1037- const pt = this . editor . svg . point ( e . clientX , e . clientY )
1038- const trimData = this . calculateTrim ( targetEl , pt )
1039-
1040- if ( trimData && trimData . preview ) {
1127+ if ( trimData . preview ) {
10411128 const p = trimData . preview
10421129
10431130 if ( p . type === 'arc' ) {
@@ -1138,9 +1225,11 @@ class TrimCommand extends Command {
11381225
11391226 cleanup ( ) {
11401227 document . removeEventListener ( 'keydown' , this . boundOnKeyDown )
1228+ document . removeEventListener ( 'contextmenu' , this . boundOnRightClick , true )
11411229 document . removeEventListener ( 'mousemove' , this . boundOnMouseMove )
11421230 this . editor . signals . toogledSelect . remove ( this . boundOnElementSelected )
11431231 this . editor . signals . toogledSelect . remove ( this . boundOnLineClicked )
1232+ this . editor . signals . commandCancelled . remove ( this . cleanup , this )
11441233 this . editor . signals . preferencesChanged . remove ( this . boundOnPreferencesChanged )
11451234
11461235 if ( this . ghostLine ) this . ghostLine . remove ( )
0 commit comments