* 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()
|
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 ({
|
test('Can delete a single segment line with keyboard', async ({
|
||||||
page,
|
page,
|
||||||
scene,
|
scene,
|
||||||
|
|||||||
@ -165,7 +165,12 @@ import type { Themes } from '@src/lib/theme'
|
|||||||
import { getThemeColorForThreeJs } from '@src/lib/theme'
|
import { getThemeColorForThreeJs } from '@src/lib/theme'
|
||||||
import { err, reportRejection, trap } from '@src/lib/trap'
|
import { err, reportRejection, trap } from '@src/lib/trap'
|
||||||
import { isArray, isOverlap, roundOff } from '@src/lib/utils'
|
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 {
|
import type {
|
||||||
SegmentOverlayPayload,
|
SegmentOverlayPayload,
|
||||||
SketchDetails,
|
SketchDetails,
|
||||||
@ -798,7 +803,7 @@ export class SceneEntities {
|
|||||||
const callExpName = _node1.node?.callee?.name.name
|
const callExpName = _node1.node?.callee?.name.name
|
||||||
|
|
||||||
const initSegment =
|
const initSegment =
|
||||||
segment.type === 'TangentialArcTo'
|
segment.type === 'TangentialArcTo' || segment.type === 'TangentialArc'
|
||||||
? segmentUtils.tangentialArc.init
|
? segmentUtils.tangentialArc.init
|
||||||
: segment.type === 'Circle'
|
: segment.type === 'Circle'
|
||||||
? segmentUtils.circle.init
|
? segmentUtils.circle.init
|
||||||
@ -3023,11 +3028,20 @@ export class SceneEntities {
|
|||||||
return input
|
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 {
|
return {
|
||||||
type: 'straight-segment',
|
type: 'straight-segment',
|
||||||
from,
|
from,
|
||||||
to: dragTo,
|
to: dragTo,
|
||||||
|
previousEndTangent: previousSegment
|
||||||
|
? findTangentDirection(previousSegment)
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3953,6 +3967,11 @@ function findTangentDirection(segmentGroup: Group) {
|
|||||||
) +
|
) +
|
||||||
(Math.PI / 2) * (segmentGroup.userData.ccw ? 1 : -1)
|
(Math.PI / 2) * (segmentGroup.userData.ccw ? 1 : -1)
|
||||||
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
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 {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
'Unsupported segment type for tangent direction calculation: ',
|
'Unsupported segment type for tangent direction calculation: ',
|
||||||
|
|||||||
@ -550,7 +550,10 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
|
|
||||||
export function getTanPreviousPoint(prevSegment: Sketch['paths'][number]) {
|
export function getTanPreviousPoint(prevSegment: Sketch['paths'][number]) {
|
||||||
let previousPoint = prevSegment.from
|
let previousPoint = prevSegment.from
|
||||||
if (prevSegment.type === 'TangentialArcTo') {
|
if (
|
||||||
|
prevSegment.type === 'TangentialArcTo' ||
|
||||||
|
prevSegment.type === 'TangentialArc'
|
||||||
|
) {
|
||||||
previousPoint = getTangentPointFromPreviousArc(
|
previousPoint = getTangentPointFromPreviousArc(
|
||||||
prevSegment.center,
|
prevSegment.center,
|
||||||
prevSegment.ccw,
|
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'
|
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
|
||||||
@ -28,6 +31,7 @@ import {
|
|||||||
createCallExpressionStdLibKw,
|
createCallExpressionStdLibKw,
|
||||||
createLabeledArg,
|
createLabeledArg,
|
||||||
createLiteral,
|
createLiteral,
|
||||||
|
createLiteralMaybeSuffix,
|
||||||
createLocalName,
|
createLocalName,
|
||||||
createPipeExpression,
|
createPipeExpression,
|
||||||
createTagDeclarator,
|
createTagDeclarator,
|
||||||
@ -83,8 +87,15 @@ import type {
|
|||||||
} from '@src/lang/wasm'
|
} from '@src/lang/wasm'
|
||||||
import { sketchFromKclValue } from '@src/lang/wasm'
|
import { sketchFromKclValue } from '@src/lang/wasm'
|
||||||
import { err } from '@src/lib/trap'
|
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 type { EdgeCutInfo } from '@src/machines/modelingMachine'
|
||||||
|
import { cross2d, distance2d, isValidNumber, subVec } from '@src/lib/utils2d'
|
||||||
|
|
||||||
const STRAIGHT_SEGMENT_ERR = () =>
|
const STRAIGHT_SEGMENT_ERR = () =>
|
||||||
new Error('Invalid input, expected "straight-segment"')
|
new Error('Invalid input, expected "straight-segment"')
|
||||||
@ -3976,7 +3987,14 @@ export function getArgForEnd(lineCall: CallExpressionKw):
|
|||||||
case 'line': {
|
case 'line': {
|
||||||
const arg = findKwArgAny(DETERMINING_ARGS, lineCall)
|
const arg = findKwArgAny(DETERMINING_ARGS, lineCall)
|
||||||
if (arg === undefined) {
|
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)
|
return getValuesForXYFns(arg)
|
||||||
}
|
}
|
||||||
@ -4145,27 +4163,101 @@ const tangentialArcHelpers = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const argLabel = isAbsolute ? ARG_END_ABSOLUTE : ARG_END
|
// All function arguments, except the tag
|
||||||
const functionName = isAbsolute ? 'tangentialArcTo' : 'tangentialArc'
|
const functionArguments = callExpression.arguments
|
||||||
|
.map((arg) => arg.label?.name)
|
||||||
|
.filter((n) => n && n !== ARG_TAG)
|
||||||
|
|
||||||
for (const arg of callExpression.arguments) {
|
if (areArraysEqual(functionArguments, [ARG_ANGLE, ARG_RADIUS])) {
|
||||||
if (arg.label?.name !== argLabel && arg.label?.name !== ARG_TAG) {
|
// 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(
|
console.debug(
|
||||||
`Trying to edit unsupported ${functionName} keyword arguments; skipping`
|
`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 {
|
return {
|
||||||
modifiedAst: _node,
|
modifiedAst: _node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import type {
|
|||||||
SourceRange,
|
SourceRange,
|
||||||
VariableMap,
|
VariableMap,
|
||||||
} from '@src/lang/wasm'
|
} from '@src/lang/wasm'
|
||||||
|
import type { Coords2d } from '@src/lang/std/sketch'
|
||||||
|
|
||||||
export interface ModifyAstBase {
|
export interface ModifyAstBase {
|
||||||
node: Node<Program>
|
node: Node<Program>
|
||||||
@ -46,6 +47,7 @@ interface StraightSegmentInput {
|
|||||||
from: [number, number]
|
from: [number, number]
|
||||||
to: [number, number]
|
to: [number, number]
|
||||||
snap?: boolean
|
snap?: boolean
|
||||||
|
previousEndTangent?: Coords2d
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Inputs for arcs, excluding tangentialArc for reasons explain in the
|
/** 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)
|
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> & {
|
export type SafeArray<T> = Omit<Array<T>, number> & {
|
||||||
[index: number]: T | undefined
|
[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(
|
export function closestPointOnRay(
|
||||||
rayOrigin: Coords2d,
|
rayOrigin: Coords2d,
|
||||||
rayDirection: Coords2d,
|
rayDirection: Coords2d,
|
||||||
pointToCheck: Coords2d,
|
pointToCheck: Coords2d,
|
||||||
allowNegative = false
|
allowNegative = false
|
||||||
) {
|
) {
|
||||||
const dirMagnitude = Math.sqrt(
|
const normalizedDir = normalizeVec(rayDirection)
|
||||||
rayDirection[0] * rayDirection[0] + rayDirection[1] * rayDirection[1]
|
const originToPoint = subVec(pointToCheck, rayOrigin)
|
||||||
)
|
|
||||||
const normalizedDir: Coords2d = [
|
|
||||||
rayDirection[0] / dirMagnitude,
|
|
||||||
rayDirection[1] / dirMagnitude,
|
|
||||||
]
|
|
||||||
|
|
||||||
const originToPoint: Coords2d = [
|
|
||||||
pointToCheck[0] - rayOrigin[0],
|
|
||||||
pointToCheck[1] - rayOrigin[1],
|
|
||||||
]
|
|
||||||
|
|
||||||
let t =
|
let t =
|
||||||
originToPoint[0] * normalizedDir[0] + originToPoint[1] * normalizedDir[1]
|
originToPoint[0] * normalizedDir[0] + originToPoint[1] * normalizedDir[1]
|
||||||
|
|||||||
Reference in New Issue
Block a user