Skip to content

Commit 832bf77

Browse files
authored
#7255 tangentialArc: angle, radius point-and-click support (#7449)
* separate handling of tangentialArc with angle and radius args * make previousEndTangent available in segment input for handling tangentialArc with angle/radius * start adding support for editing tangentialArc with angle, radius * draw tangentialArc sketch when using angle, radius * fix getTanPreviousPoint when using tangentialArc with angle, radius * fix case of unwanted negative angles when calculating angle for tangentialArc * lint * add test for tangentialArc dragging with andle, radius * lint, fmt * fix getArgForEnd for tangentialArc with radius, angle * renaming vars
1 parent acb43fc commit 832bf77

File tree

7 files changed

+219
-33
lines changed

7 files changed

+219
-33
lines changed

e2e/playwright/sketch-tests.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,48 @@ solid001 = subtract([extrude001], tools = [extrude002])
14451445
await u.closeDebugPanel()
14461446
})
14471447

1448+
test('Can edit a tangentialArc defined by angle and radius', async ({
1449+
page,
1450+
homePage,
1451+
editor,
1452+
toolbar,
1453+
scene,
1454+
cmdBar,
1455+
}) => {
1456+
const viewportSize = { width: 1500, height: 750 }
1457+
await page.setBodyDimensions(viewportSize)
1458+
1459+
await page.addInitScript(async () => {
1460+
localStorage.setItem(
1461+
'persistCode',
1462+
`@settings(defaultLengthUnit=in)
1463+
sketch001 = startSketchOn(XZ)
1464+
|> startProfile(at = [-10, -10])
1465+
|> line(end = [20.0, 10.0])
1466+
|> tangentialArc(angle = 60deg, radius=10.0)`
1467+
)
1468+
})
1469+
1470+
await homePage.goToModelingScene()
1471+
await toolbar.waitForFeatureTreeToBeBuilt()
1472+
await scene.settled(cmdBar)
1473+
1474+
await (await toolbar.getFeatureTreeOperation('Sketch', 0)).dblclick()
1475+
1476+
await page.waitForTimeout(1000)
1477+
1478+
await page.mouse.move(1200, 139)
1479+
await page.mouse.down()
1480+
await page.mouse.move(870, 250)
1481+
1482+
await page.waitForTimeout(200)
1483+
1484+
await editor.expectEditor.toContain(
1485+
`tangentialArc(angle = 234.01deg, radius = 4.08)`,
1486+
{ shouldNormalise: true }
1487+
)
1488+
})
1489+
14481490
test('Can delete a single segment line with keyboard', async ({
14491491
page,
14501492
scene,

src/clientSideScene/sceneEntities.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,12 @@ import type { Themes } from '@src/lib/theme'
165165
import { getThemeColorForThreeJs } from '@src/lib/theme'
166166
import { err, reportRejection, trap } from '@src/lib/trap'
167167
import { isArray, isOverlap, roundOff } from '@src/lib/utils'
168-
import { closestPointOnRay, deg2Rad } from '@src/lib/utils2d'
168+
import {
169+
closestPointOnRay,
170+
deg2Rad,
171+
normalizeVec,
172+
subVec,
173+
} from '@src/lib/utils2d'
169174
import type {
170175
SegmentOverlayPayload,
171176
SketchDetails,
@@ -798,7 +803,7 @@ export class SceneEntities {
798803
const callExpName = _node1.node?.callee?.name.name
799804

800805
const initSegment =
801-
segment.type === 'TangentialArcTo'
806+
segment.type === 'TangentialArcTo' || segment.type === 'TangentialArc'
802807
? segmentUtils.tangentialArc.init
803808
: segment.type === 'Circle'
804809
? segmentUtils.circle.init
@@ -3023,11 +3028,20 @@ export class SceneEntities {
30233028
return input
30243029
}
30253030

3026-
// straight segment is the default
3031+
// straight segment is the default,
3032+
// this includes "tangential-arc-to-segment"
3033+
3034+
const segments: SafeArray<Group> = Object.values(this.activeSegments) // Using the order in the object feels wrong
3035+
const currentIndex = segments.indexOf(group)
3036+
const previousSegment = segments[currentIndex - 1]
3037+
30273038
return {
30283039
type: 'straight-segment',
30293040
from,
30303041
to: dragTo,
3042+
previousEndTangent: previousSegment
3043+
? findTangentDirection(previousSegment)
3044+
: undefined,
30313045
}
30323046
}
30333047

@@ -3953,6 +3967,11 @@ function findTangentDirection(segmentGroup: Group) {
39533967
) +
39543968
(Math.PI / 2) * (segmentGroup.userData.ccw ? 1 : -1)
39553969
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
3970+
} else if (segmentGroup.userData.type === STRAIGHT_SEGMENT) {
3971+
const to = segmentGroup.userData.to as Coords2d
3972+
const from = segmentGroup.userData.from as Coords2d
3973+
tangentDirection = subVec(to, from)
3974+
tangentDirection = normalizeVec(tangentDirection)
39563975
} else {
39573976
console.warn(
39583977
'Unsupported segment type for tangent direction calculation: ',

src/clientSideScene/segments.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,10 @@ class TangentialArcToSegment implements SegmentUtils {
550550

551551
export function getTanPreviousPoint(prevSegment: Sketch['paths'][number]) {
552552
let previousPoint = prevSegment.from
553-
if (prevSegment.type === 'TangentialArcTo') {
553+
if (
554+
prevSegment.type === 'TangentialArcTo' ||
555+
prevSegment.type === 'TangentialArc'
556+
) {
554557
previousPoint = getTangentPointFromPreviousArc(
555558
prevSegment.center,
556559
prevSegment.ccw,

src/lang/std/sketch.ts

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { perpendicularDistance } from 'sketch-helpers'
1+
import {
2+
calculateIntersectionOfTwoLines,
3+
perpendicularDistance,
4+
} from 'sketch-helpers'
25

36
import type { Node } from '@rust/kcl-lib/bindings/Node'
47

@@ -28,6 +31,7 @@ import {
2831
createCallExpressionStdLibKw,
2932
createLabeledArg,
3033
createLiteral,
34+
createLiteralMaybeSuffix,
3135
createLocalName,
3236
createPipeExpression,
3337
createTagDeclarator,
@@ -83,8 +87,15 @@ import type {
8387
} from '@src/lang/wasm'
8488
import { sketchFromKclValue } from '@src/lang/wasm'
8589
import { err } from '@src/lib/trap'
86-
import { allLabels, getAngle, getLength, roundOff } from '@src/lib/utils'
90+
import {
91+
allLabels,
92+
areArraysEqual,
93+
getAngle,
94+
getLength,
95+
roundOff,
96+
} from '@src/lib/utils'
8797
import type { EdgeCutInfo } from '@src/machines/modelingMachine'
98+
import { cross2d, distance2d, isValidNumber, subVec } from '@src/lib/utils2d'
8899

89100
const STRAIGHT_SEGMENT_ERR = () =>
90101
new Error('Invalid input, expected "straight-segment"')
@@ -3976,7 +3987,14 @@ export function getArgForEnd(lineCall: CallExpressionKw):
39763987
case 'line': {
39773988
const arg = findKwArgAny(DETERMINING_ARGS, lineCall)
39783989
if (arg === undefined) {
3979-
return new Error("no end of the line was found in fn '" + name + "'")
3990+
const angle = findKwArg(ARG_ANGLE, lineCall)
3991+
const radius = findKwArg(ARG_RADIUS, lineCall)
3992+
if (name === 'tangentialArc' && angle && radius) {
3993+
// tangentialArc may use angle and radius instead of end
3994+
return { val: [angle, radius], tag: findKwArg(ARG_TAG, lineCall) }
3995+
} else {
3996+
return new Error("no end of the line was found in fn '" + name + "'")
3997+
}
39803998
}
39813999
return getValuesForXYFns(arg)
39824000
}
@@ -4145,27 +4163,101 @@ const tangentialArcHelpers = {
41454163
)
41464164
}
41474165

4148-
const argLabel = isAbsolute ? ARG_END_ABSOLUTE : ARG_END
4149-
const functionName = isAbsolute ? 'tangentialArcTo' : 'tangentialArc'
4166+
// All function arguments, except the tag
4167+
const functionArguments = callExpression.arguments
4168+
.map((arg) => arg.label?.name)
4169+
.filter((n) => n && n !== ARG_TAG)
4170+
4171+
if (areArraysEqual(functionArguments, [ARG_ANGLE, ARG_RADIUS])) {
4172+
// Using length and radius -> convert "from", "to" to the matching length and radius
4173+
const previousEndTangent = input.previousEndTangent
4174+
if (previousEndTangent) {
4175+
// Find a circle with these two lines:
4176+
// - We know "from" and "to" are on the circle, so we can use their perpendicular bisector as the first line
4177+
// - The second line goes from "from" to the tangentRotated direction
4178+
// Intersecting these two lines will give us the center of the circle.
4179+
4180+
// line 1
4181+
const midPoint: [number, number] = [
4182+
(from[0] + to[0]) / 2,
4183+
(from[1] + to[1]) / 2,
4184+
]
4185+
const dir = subVec(to, from)
4186+
const perpDir = [-dir[1], dir[0]]
4187+
const line1PointB: Coords2d = [
4188+
midPoint[0] + perpDir[0],
4189+
midPoint[1] + perpDir[1],
4190+
]
41504191

4151-
for (const arg of callExpression.arguments) {
4152-
if (arg.label?.name !== argLabel && arg.label?.name !== ARG_TAG) {
4192+
// line 2
4193+
const tangentRotated: Coords2d = [
4194+
-previousEndTangent[1],
4195+
previousEndTangent[0],
4196+
]
4197+
4198+
const center = calculateIntersectionOfTwoLines({
4199+
line1: [midPoint, line1PointB],
4200+
line2Point: from,
4201+
line2Angle: getAngle([0, 0], tangentRotated),
4202+
})
4203+
if (isValidNumber(center[0]) && isValidNumber(center[1])) {
4204+
// We have the circle center, calculate the angle by calculating the angle for "from" and "to" points
4205+
// These are in the range of [-180, 180] degrees
4206+
const angleFrom = getAngle(center, from)
4207+
const angleTo = getAngle(center, to)
4208+
let angle = angleTo - angleFrom
4209+
4210+
// Handle the cases where the angle would have an undesired sign.
4211+
// If the circle is CCW we want the angle to be always positive, otherwise negative.
4212+
// eg. CCW: angleFrom is -90 and angleTo is -175 -> would be -85, but we want it to be 275
4213+
const isCCW = cross2d(previousEndTangent, dir) > 0
4214+
if (isCCW) {
4215+
angle = (angle + 360) % 360 // Ensure angle is positive
4216+
} else {
4217+
angle = (angle - 360) % 360 // Ensure angle is negative
4218+
}
4219+
4220+
const radius = distance2d(center, from)
4221+
4222+
mutateKwArg(
4223+
ARG_RADIUS,
4224+
callExpression,
4225+
createLiteral(roundOff(radius, 2))
4226+
)
4227+
const angleValue = createLiteralMaybeSuffix({
4228+
value: roundOff(angle, 2),
4229+
suffix: 'Deg',
4230+
})
4231+
if (!err(angleValue)) {
4232+
mutateKwArg(ARG_ANGLE, callExpression, angleValue)
4233+
}
4234+
} else {
4235+
console.debug('Invalid center calculated for tangential arc')
4236+
}
4237+
} else {
4238+
console.debug('No previous end tangent found, cannot calculate radius')
4239+
}
4240+
} else {
4241+
const argLabel = isAbsolute ? ARG_END_ABSOLUTE : ARG_END
4242+
if (areArraysEqual(functionArguments, [argLabel])) {
4243+
// Using end or endAbsolute
4244+
const toArrExp = createArrayExpression([
4245+
createLiteral(roundOff(isAbsolute ? to[0] : to[0] - from[0], 2)),
4246+
createLiteral(roundOff(isAbsolute ? to[1] : to[1] - from[1], 2)),
4247+
])
4248+
4249+
mutateKwArg(argLabel, callExpression, toArrExp)
4250+
} else {
4251+
// Unsupported arguments
4252+
const functionName =
4253+
callExpression.callee.name.name ??
4254+
(isAbsolute ? 'tangentialArcTo' : 'tangentialArc')
41534255
console.debug(
41544256
`Trying to edit unsupported ${functionName} keyword arguments; skipping`
41554257
)
4156-
return {
4157-
modifiedAst: _node,
4158-
pathToNode,
4159-
}
41604258
}
41614259
}
41624260

4163-
const toArrExp = createArrayExpression([
4164-
createLiteral(roundOff(isAbsolute ? to[0] : to[0] - from[0], 2)),
4165-
createLiteral(roundOff(isAbsolute ? to[1] : to[1] - from[1], 2)),
4166-
])
4167-
4168-
mutateKwArg(argLabel, callExpression, toArrExp)
41694261
return {
41704262
modifiedAst: _node,
41714263
pathToNode,

src/lang/std/stdTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
SourceRange,
2424
VariableMap,
2525
} from '@src/lang/wasm'
26+
import type { Coords2d } from '@src/lang/std/sketch'
2627

2728
export interface ModifyAstBase {
2829
node: Node<Program>
@@ -46,6 +47,7 @@ interface StraightSegmentInput {
4647
from: [number, number]
4748
to: [number, number]
4849
snap?: boolean
50+
previousEndTangent?: Coords2d
4951
}
5052

5153
/** Inputs for arcs, excluding tangentialArc for reasons explain in the

src/lib/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ export function isArray(val: any): val is unknown[] {
5555
return Array.isArray(val)
5656
}
5757

58+
export function areArraysEqual<T>(a: T[], b: T[]): boolean {
59+
if (a.length !== b.length) return false
60+
const set1 = new Set(a)
61+
return b.every((element) => set1.has(element))
62+
}
63+
5864
export type SafeArray<T> = Omit<Array<T>, number> & {
5965
[index: number]: T | undefined
6066
}

src/lib/utils2d.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,46 @@ export function getTangentPointFromPreviousArc(
1818
]
1919
}
2020

21+
export function subVec(a: Coords2d, b: Coords2d): Coords2d {
22+
return [a[0] - b[0], a[1] - b[1]]
23+
}
24+
25+
export function normalizeVec(v: Coords2d): Coords2d {
26+
const magnitude = Math.sqrt(v[0] * v[0] + v[1] * v[1])
27+
if (magnitude === 0) {
28+
return [0, 0]
29+
}
30+
return [v[0] / magnitude, v[1] / magnitude]
31+
}
32+
33+
export function cross2d(a: Coords2d, b: Coords2d): number {
34+
return a[0] * b[1] - a[1] * b[0]
35+
}
36+
37+
export function distance2d(a: Coords2d, b: Coords2d): number {
38+
const dx = a[0] - b[0]
39+
const dy = a[1] - b[1]
40+
return Math.sqrt(dx * dx + dy * dy)
41+
}
42+
43+
export function isValidNumber(value: number): boolean {
44+
return typeof value === 'number' && !Number.isNaN(value) && isFinite(value)
45+
}
46+
47+
export function rotateVec(v: Coords2d, rad: number): Coords2d {
48+
const cos = Math.cos(rad)
49+
const sin = Math.sin(rad)
50+
return [v[0] * cos - v[1] * sin, v[0] * sin + v[1] * cos]
51+
}
52+
2153
export function closestPointOnRay(
2254
rayOrigin: Coords2d,
2355
rayDirection: Coords2d,
2456
pointToCheck: Coords2d,
2557
allowNegative = false
2658
) {
27-
const dirMagnitude = Math.sqrt(
28-
rayDirection[0] * rayDirection[0] + rayDirection[1] * rayDirection[1]
29-
)
30-
const normalizedDir: Coords2d = [
31-
rayDirection[0] / dirMagnitude,
32-
rayDirection[1] / dirMagnitude,
33-
]
34-
35-
const originToPoint: Coords2d = [
36-
pointToCheck[0] - rayOrigin[0],
37-
pointToCheck[1] - rayOrigin[1],
38-
]
59+
const normalizedDir = normalizeVec(rayDirection)
60+
const originToPoint = subVec(pointToCheck, rayOrigin)
3961

4062
let t =
4163
originToPoint[0] * normalizedDir[0] + originToPoint[1] * normalizedDir[1]

0 commit comments

Comments
 (0)