* 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
This commit is contained in:
@ -1445,6 +1445,48 @@ solid001 = subtract([extrude001], tools = [extrude002])
|
||||
await u.closeDebugPanel()
|
||||
})
|
||||
|
||||
test('Can edit a tangentialArc defined by angle and radius', async ({
|
||||
page,
|
||||
homePage,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const viewportSize = { width: 1500, height: 750 }
|
||||
await page.setBodyDimensions(viewportSize)
|
||||
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`@settings(defaultLengthUnit=in)
|
||||
sketch001 = startSketchOn(XZ)
|
||||
|> startProfile(at = [-10, -10])
|
||||
|> line(end = [20.0, 10.0])
|
||||
|> tangentialArc(angle = 60deg, radius=10.0)`
|
||||
)
|
||||
})
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await toolbar.waitForFeatureTreeToBeBuilt()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await (await toolbar.getFeatureTreeOperation('Sketch', 0)).dblclick()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
await page.mouse.move(1200, 139)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(870, 250)
|
||||
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
await editor.expectEditor.toContain(
|
||||
`tangentialArc(angle = 234.01deg, radius = 4.08)`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
})
|
||||
|
||||
test('Can delete a single segment line with keyboard', async ({
|
||||
page,
|
||||
scene,
|
||||
|
@ -165,7 +165,12 @@ import type { Themes } from '@src/lib/theme'
|
||||
import { getThemeColorForThreeJs } from '@src/lib/theme'
|
||||
import { err, reportRejection, trap } from '@src/lib/trap'
|
||||
import { isArray, isOverlap, roundOff } from '@src/lib/utils'
|
||||
import { closestPointOnRay, deg2Rad } from '@src/lib/utils2d'
|
||||
import {
|
||||
closestPointOnRay,
|
||||
deg2Rad,
|
||||
normalizeVec,
|
||||
subVec,
|
||||
} from '@src/lib/utils2d'
|
||||
import type {
|
||||
SegmentOverlayPayload,
|
||||
SketchDetails,
|
||||
@ -798,7 +803,7 @@ export class SceneEntities {
|
||||
const callExpName = _node1.node?.callee?.name.name
|
||||
|
||||
const initSegment =
|
||||
segment.type === 'TangentialArcTo'
|
||||
segment.type === 'TangentialArcTo' || segment.type === 'TangentialArc'
|
||||
? segmentUtils.tangentialArc.init
|
||||
: segment.type === 'Circle'
|
||||
? segmentUtils.circle.init
|
||||
@ -3023,11 +3028,20 @@ export class SceneEntities {
|
||||
return input
|
||||
}
|
||||
|
||||
// straight segment is the default
|
||||
// straight segment is the default,
|
||||
// this includes "tangential-arc-to-segment"
|
||||
|
||||
const segments: SafeArray<Group> = Object.values(this.activeSegments) // Using the order in the object feels wrong
|
||||
const currentIndex = segments.indexOf(group)
|
||||
const previousSegment = segments[currentIndex - 1]
|
||||
|
||||
return {
|
||||
type: 'straight-segment',
|
||||
from,
|
||||
to: dragTo,
|
||||
previousEndTangent: previousSegment
|
||||
? findTangentDirection(previousSegment)
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3953,6 +3967,11 @@ function findTangentDirection(segmentGroup: Group) {
|
||||
) +
|
||||
(Math.PI / 2) * (segmentGroup.userData.ccw ? 1 : -1)
|
||||
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
||||
} else if (segmentGroup.userData.type === STRAIGHT_SEGMENT) {
|
||||
const to = segmentGroup.userData.to as Coords2d
|
||||
const from = segmentGroup.userData.from as Coords2d
|
||||
tangentDirection = subVec(to, from)
|
||||
tangentDirection = normalizeVec(tangentDirection)
|
||||
} else {
|
||||
console.warn(
|
||||
'Unsupported segment type for tangent direction calculation: ',
|
||||
|
@ -550,7 +550,10 @@ class TangentialArcToSegment implements SegmentUtils {
|
||||
|
||||
export function getTanPreviousPoint(prevSegment: Sketch['paths'][number]) {
|
||||
let previousPoint = prevSegment.from
|
||||
if (prevSegment.type === 'TangentialArcTo') {
|
||||
if (
|
||||
prevSegment.type === 'TangentialArcTo' ||
|
||||
prevSegment.type === 'TangentialArc'
|
||||
) {
|
||||
previousPoint = getTangentPointFromPreviousArc(
|
||||
prevSegment.center,
|
||||
prevSegment.ccw,
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { perpendicularDistance } from 'sketch-helpers'
|
||||
import {
|
||||
calculateIntersectionOfTwoLines,
|
||||
perpendicularDistance,
|
||||
} from 'sketch-helpers'
|
||||
|
||||
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||||
|
||||
@ -28,6 +31,7 @@ import {
|
||||
createCallExpressionStdLibKw,
|
||||
createLabeledArg,
|
||||
createLiteral,
|
||||
createLiteralMaybeSuffix,
|
||||
createLocalName,
|
||||
createPipeExpression,
|
||||
createTagDeclarator,
|
||||
@ -83,8 +87,15 @@ import type {
|
||||
} from '@src/lang/wasm'
|
||||
import { sketchFromKclValue } from '@src/lang/wasm'
|
||||
import { err } from '@src/lib/trap'
|
||||
import { allLabels, getAngle, getLength, roundOff } from '@src/lib/utils'
|
||||
import {
|
||||
allLabels,
|
||||
areArraysEqual,
|
||||
getAngle,
|
||||
getLength,
|
||||
roundOff,
|
||||
} from '@src/lib/utils'
|
||||
import type { EdgeCutInfo } from '@src/machines/modelingMachine'
|
||||
import { cross2d, distance2d, isValidNumber, subVec } from '@src/lib/utils2d'
|
||||
|
||||
const STRAIGHT_SEGMENT_ERR = () =>
|
||||
new Error('Invalid input, expected "straight-segment"')
|
||||
@ -3976,7 +3987,14 @@ export function getArgForEnd(lineCall: CallExpressionKw):
|
||||
case 'line': {
|
||||
const arg = findKwArgAny(DETERMINING_ARGS, lineCall)
|
||||
if (arg === undefined) {
|
||||
return new Error("no end of the line was found in fn '" + name + "'")
|
||||
const angle = findKwArg(ARG_ANGLE, lineCall)
|
||||
const radius = findKwArg(ARG_RADIUS, lineCall)
|
||||
if (name === 'tangentialArc' && angle && radius) {
|
||||
// tangentialArc may use angle and radius instead of end
|
||||
return { val: [angle, radius], tag: findKwArg(ARG_TAG, lineCall) }
|
||||
} else {
|
||||
return new Error("no end of the line was found in fn '" + name + "'")
|
||||
}
|
||||
}
|
||||
return getValuesForXYFns(arg)
|
||||
}
|
||||
@ -4145,27 +4163,101 @@ const tangentialArcHelpers = {
|
||||
)
|
||||
}
|
||||
|
||||
const argLabel = isAbsolute ? ARG_END_ABSOLUTE : ARG_END
|
||||
const functionName = isAbsolute ? 'tangentialArcTo' : 'tangentialArc'
|
||||
// All function arguments, except the tag
|
||||
const functionArguments = callExpression.arguments
|
||||
.map((arg) => arg.label?.name)
|
||||
.filter((n) => n && n !== ARG_TAG)
|
||||
|
||||
for (const arg of callExpression.arguments) {
|
||||
if (arg.label?.name !== argLabel && arg.label?.name !== ARG_TAG) {
|
||||
if (areArraysEqual(functionArguments, [ARG_ANGLE, ARG_RADIUS])) {
|
||||
// Using length and radius -> convert "from", "to" to the matching length and radius
|
||||
const previousEndTangent = input.previousEndTangent
|
||||
if (previousEndTangent) {
|
||||
// Find a circle with these two lines:
|
||||
// - We know "from" and "to" are on the circle, so we can use their perpendicular bisector as the first line
|
||||
// - The second line goes from "from" to the tangentRotated direction
|
||||
// Intersecting these two lines will give us the center of the circle.
|
||||
|
||||
// line 1
|
||||
const midPoint: [number, number] = [
|
||||
(from[0] + to[0]) / 2,
|
||||
(from[1] + to[1]) / 2,
|
||||
]
|
||||
const dir = subVec(to, from)
|
||||
const perpDir = [-dir[1], dir[0]]
|
||||
const line1PointB: Coords2d = [
|
||||
midPoint[0] + perpDir[0],
|
||||
midPoint[1] + perpDir[1],
|
||||
]
|
||||
|
||||
// line 2
|
||||
const tangentRotated: Coords2d = [
|
||||
-previousEndTangent[1],
|
||||
previousEndTangent[0],
|
||||
]
|
||||
|
||||
const center = calculateIntersectionOfTwoLines({
|
||||
line1: [midPoint, line1PointB],
|
||||
line2Point: from,
|
||||
line2Angle: getAngle([0, 0], tangentRotated),
|
||||
})
|
||||
if (isValidNumber(center[0]) && isValidNumber(center[1])) {
|
||||
// We have the circle center, calculate the angle by calculating the angle for "from" and "to" points
|
||||
// These are in the range of [-180, 180] degrees
|
||||
const angleFrom = getAngle(center, from)
|
||||
const angleTo = getAngle(center, to)
|
||||
let angle = angleTo - angleFrom
|
||||
|
||||
// Handle the cases where the angle would have an undesired sign.
|
||||
// If the circle is CCW we want the angle to be always positive, otherwise negative.
|
||||
// eg. CCW: angleFrom is -90 and angleTo is -175 -> would be -85, but we want it to be 275
|
||||
const isCCW = cross2d(previousEndTangent, dir) > 0
|
||||
if (isCCW) {
|
||||
angle = (angle + 360) % 360 // Ensure angle is positive
|
||||
} else {
|
||||
angle = (angle - 360) % 360 // Ensure angle is negative
|
||||
}
|
||||
|
||||
const radius = distance2d(center, from)
|
||||
|
||||
mutateKwArg(
|
||||
ARG_RADIUS,
|
||||
callExpression,
|
||||
createLiteral(roundOff(radius, 2))
|
||||
)
|
||||
const angleValue = createLiteralMaybeSuffix({
|
||||
value: roundOff(angle, 2),
|
||||
suffix: 'Deg',
|
||||
})
|
||||
if (!err(angleValue)) {
|
||||
mutateKwArg(ARG_ANGLE, callExpression, angleValue)
|
||||
}
|
||||
} else {
|
||||
console.debug('Invalid center calculated for tangential arc')
|
||||
}
|
||||
} else {
|
||||
console.debug('No previous end tangent found, cannot calculate radius')
|
||||
}
|
||||
} else {
|
||||
const argLabel = isAbsolute ? ARG_END_ABSOLUTE : ARG_END
|
||||
if (areArraysEqual(functionArguments, [argLabel])) {
|
||||
// Using end or endAbsolute
|
||||
const toArrExp = createArrayExpression([
|
||||
createLiteral(roundOff(isAbsolute ? to[0] : to[0] - from[0], 2)),
|
||||
createLiteral(roundOff(isAbsolute ? to[1] : to[1] - from[1], 2)),
|
||||
])
|
||||
|
||||
mutateKwArg(argLabel, callExpression, toArrExp)
|
||||
} else {
|
||||
// Unsupported arguments
|
||||
const functionName =
|
||||
callExpression.callee.name.name ??
|
||||
(isAbsolute ? 'tangentialArcTo' : 'tangentialArc')
|
||||
console.debug(
|
||||
`Trying to edit unsupported ${functionName} keyword arguments; skipping`
|
||||
)
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toArrExp = createArrayExpression([
|
||||
createLiteral(roundOff(isAbsolute ? to[0] : to[0] - from[0], 2)),
|
||||
createLiteral(roundOff(isAbsolute ? to[1] : to[1] - from[1], 2)),
|
||||
])
|
||||
|
||||
mutateKwArg(argLabel, callExpression, toArrExp)
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
|
@ -23,6 +23,7 @@ import type {
|
||||
SourceRange,
|
||||
VariableMap,
|
||||
} from '@src/lang/wasm'
|
||||
import type { Coords2d } from '@src/lang/std/sketch'
|
||||
|
||||
export interface ModifyAstBase {
|
||||
node: Node<Program>
|
||||
@ -46,6 +47,7 @@ interface StraightSegmentInput {
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
snap?: boolean
|
||||
previousEndTangent?: Coords2d
|
||||
}
|
||||
|
||||
/** Inputs for arcs, excluding tangentialArc for reasons explain in the
|
||||
|
@ -55,6 +55,12 @@ export function isArray(val: any): val is unknown[] {
|
||||
return Array.isArray(val)
|
||||
}
|
||||
|
||||
export function areArraysEqual<T>(a: T[], b: T[]): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
const set1 = new Set(a)
|
||||
return b.every((element) => set1.has(element))
|
||||
}
|
||||
|
||||
export type SafeArray<T> = Omit<Array<T>, number> & {
|
||||
[index: number]: T | undefined
|
||||
}
|
||||
|
@ -18,24 +18,46 @@ export function getTangentPointFromPreviousArc(
|
||||
]
|
||||
}
|
||||
|
||||
export function subVec(a: Coords2d, b: Coords2d): Coords2d {
|
||||
return [a[0] - b[0], a[1] - b[1]]
|
||||
}
|
||||
|
||||
export function normalizeVec(v: Coords2d): Coords2d {
|
||||
const magnitude = Math.sqrt(v[0] * v[0] + v[1] * v[1])
|
||||
if (magnitude === 0) {
|
||||
return [0, 0]
|
||||
}
|
||||
return [v[0] / magnitude, v[1] / magnitude]
|
||||
}
|
||||
|
||||
export function cross2d(a: Coords2d, b: Coords2d): number {
|
||||
return a[0] * b[1] - a[1] * b[0]
|
||||
}
|
||||
|
||||
export function distance2d(a: Coords2d, b: Coords2d): number {
|
||||
const dx = a[0] - b[0]
|
||||
const dy = a[1] - b[1]
|
||||
return Math.sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
|
||||
export function isValidNumber(value: number): boolean {
|
||||
return typeof value === 'number' && !Number.isNaN(value) && isFinite(value)
|
||||
}
|
||||
|
||||
export function rotateVec(v: Coords2d, rad: number): Coords2d {
|
||||
const cos = Math.cos(rad)
|
||||
const sin = Math.sin(rad)
|
||||
return [v[0] * cos - v[1] * sin, v[0] * sin + v[1] * cos]
|
||||
}
|
||||
|
||||
export function closestPointOnRay(
|
||||
rayOrigin: Coords2d,
|
||||
rayDirection: Coords2d,
|
||||
pointToCheck: Coords2d,
|
||||
allowNegative = false
|
||||
) {
|
||||
const dirMagnitude = Math.sqrt(
|
||||
rayDirection[0] * rayDirection[0] + rayDirection[1] * rayDirection[1]
|
||||
)
|
||||
const normalizedDir: Coords2d = [
|
||||
rayDirection[0] / dirMagnitude,
|
||||
rayDirection[1] / dirMagnitude,
|
||||
]
|
||||
|
||||
const originToPoint: Coords2d = [
|
||||
pointToCheck[0] - rayOrigin[0],
|
||||
pointToCheck[1] - rayOrigin[1],
|
||||
]
|
||||
const normalizedDir = normalizeVec(rayDirection)
|
||||
const originToPoint = subVec(pointToCheck, rayOrigin)
|
||||
|
||||
let t =
|
||||
originToPoint[0] * normalizedDir[0] + originToPoint[1] * normalizedDir[1]
|
||||
|
Reference in New Issue
Block a user