Skip to content

Commit 9ce4a02

Browse files
committed
fix trim
1 parent 2e5f410 commit 9ce4a02

1 file changed

Lines changed: 132 additions & 43 deletions

File tree

src/js/commands/TrimCommand.js

Lines changed: 132 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)