[BUG] circle and threePointArc and other overlay fixes (#6409)

* fix length constrainting

* conflicet

* fix circle center constraints

* fix up circle remove constraints more and add test

* fix three point arc overlays and add test for it

* fixes

* console log

* fix tangential arc stuff

* fmt

* fix unit test

* fix console error when selectiong arc
This commit is contained in:
Kurt Hutten
2025-04-30 12:08:45 +10:00
committed by GitHub
parent 0ea0d1703e
commit bf63b21d74
13 changed files with 877 additions and 190 deletions

View File

@ -1455,4 +1455,328 @@ part001 = startSketchOn(XZ)
}) })
} }
}) })
test.describe('Testing with showAllOverlays flag', () => {
test('circle overlay constraints with showAllOverlays', async ({
page,
editor,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`myvar = -141
sketch001 = startSketchOn(XZ)
profile002 = circle(sketch001, center = [345, 0], radius = 238.38)
`
)
// Set flag to always show overlays without hover
localStorage.setItem('showAllOverlays', 'true')
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
// Click on the circle line to enter edit mode
await page
.getByText('circle(sketch001, center = [345, 0], radius = 238.38)')
.click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
// Verify that the overlay is visible without hovering
await expect(page.getByTestId('segment-overlay')).toHaveCount(1)
// First, constrain the X coordinate
const xConstraintBtn = page.locator(
'[data-constraint-type="xAbsolute"][data-is-constrained="false"]'
)
await expect(xConstraintBtn).toBeVisible()
await xConstraintBtn.click()
// Complete the command
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
// Verify the X constraint was added
await editor.expectEditor.toContain('center = [xAbs001, 0]', {
shouldNormalise: true,
})
// Now constrain the Y coordinate
const yConstraintBtn = page.locator(
'[data-constraint-type="yAbsolute"][data-is-constrained="false"]'
)
await expect(yConstraintBtn).toBeVisible()
await yConstraintBtn.click()
// Complete the command
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
// Verify the Y constraint was added
await editor.expectEditor.toContain('center = [xAbs001, yAbs001]', {
shouldNormalise: true,
})
// Now constrain the radius
const radiusConstraintBtn = page.locator(
'[data-constraint-type="radius"][data-is-constrained="false"]'
)
await expect(radiusConstraintBtn).toBeVisible()
await radiusConstraintBtn.click()
// Complete the command
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
// Verify all constraints were added
await editor.expectEditor.toContain(
'center = [xAbs001, yAbs001], radius = radius001',
{ shouldNormalise: true }
)
// Now unconstrain the X coordinate
const constrainedXBtn = page.locator(
'[data-constraint-type="xAbsolute"][data-is-constrained="true"]'
)
await expect(constrainedXBtn).toBeVisible()
await constrainedXBtn.click()
// Verify the X constraint was removed
await editor.expectEditor.toContain(
'center = [345, yAbs001], radius = radius001',
{ shouldNormalise: true }
)
// Now unconstrain the Y coordinate
const constrainedYBtn = page.locator(
'[data-constraint-type="yAbsolute"][data-is-constrained="true"]'
)
await expect(constrainedYBtn).toBeVisible()
await constrainedYBtn.click()
// Verify the Y constraint was removed
await editor.expectEditor.toContain(
'center = [345, 0], radius = radius001',
{ shouldNormalise: true }
)
// Finally, unconstrain the radius
const constrainedRadiusBtn = page.locator(
'[data-constraint-type="radius"][data-is-constrained="true"]'
)
await expect(constrainedRadiusBtn).toBeVisible()
await constrainedRadiusBtn.click()
// Verify all constraints were removed
await editor.expectEditor.toContain(
'center = [345, 0], radius = 238.38',
{ shouldNormalise: true }
)
})
})
test('arc with interiorAbsolute and endAbsolute kwargs overlay constraints', async ({
page,
editor,
homePage,
scene,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`myvar = 141
sketch001 = startSketchOn(XZ)
profile001 = circleThreePoint(
sketch001,
p1 = [445.16, 202.16],
p2 = [445.16, 116.92],
p3 = [546.85, 103],
)
profile003 = startProfileAt([64.39, 35.16], sketch001)
|> line(end = [60.69, 23.02])
|> arc(interiorAbsolute = [159.26, 100.58], endAbsolute = [237.05, 84.07])
|> line(end = [70.31, 42.28])`
)
// Set flag to always show overlays without hover
localStorage.setItem('showAllOverlays', 'true')
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
await scene.settled(cmdBar)
// Click on the line before the arc to enter edit mode
await page.getByText('line(end = [60.69, 23.02])').click()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500)
// Verify overlays are visible
// 3 for the three point arc, and 4 for the 3 segments (arc has two)
await expect(page.getByTestId('segment-overlay')).toHaveCount(7)
// ---- Testing interior point constraints ----
// 1. Constrain interior X coordinate
const interiorXConstraintBtn = page
.locator(
'[data-constraint-type="xAbsolute"][data-is-constrained="false"]'
)
.nth(3)
await expect(interiorXConstraintBtn).toBeVisible()
await interiorXConstraintBtn.click()
// Complete the command
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
// Verify the constraint was added
await editor.expectEditor.toContain(
'interiorAbsolute = [xAbs001, 100.58]',
{
shouldNormalise: true,
}
)
// 2. Constrain interior Y coordinate
const interiorYConstraintBtn = page
.locator(
'[data-constraint-type="yAbsolute"][data-is-constrained="false"]'
)
.nth(3)
await expect(interiorYConstraintBtn).toBeVisible()
await interiorYConstraintBtn.click()
// Complete the command
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
// Verify both constraints were added
await editor.expectEditor.toContain(
'interiorAbsolute = [xAbs001, yAbs001]',
{
shouldNormalise: true,
}
)
// ---- Testing end point constraints ----
// 3. Constrain end X coordinate
const endXConstraintBtn = page
.locator(
'[data-constraint-type="xAbsolute"][data-is-constrained="false"]'
)
.nth(3) // still number 3 because the interior ones are now constrained
await expect(endXConstraintBtn).toBeVisible()
await endXConstraintBtn.click()
// Complete the command
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
// Verify the constraint was added
await editor.expectEditor.toContain('endAbsolute = [xAbs002, 84.07]', {
shouldNormalise: true,
})
// 4. Constrain end Y coordinate
const endYConstraintBtn = page
.locator(
'[data-constraint-type="yAbsolute"][data-is-constrained="false"]'
)
.nth(3) // still number 3 because the interior ones are now constrained
await expect(endYConstraintBtn).toBeVisible()
await endYConstraintBtn.click()
// Complete the command
await expect(
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page.getByRole('button', { name: 'arrow right Continue' }).click()
// Verify all constraints were added
await editor.expectEditor.toContain(
'interiorAbsolute = [xAbs001, yAbs001], endAbsolute = [xAbs002, yAbs002]',
{
shouldNormalise: true,
}
)
// ---- Unconstrain the coordinates in reverse order ----
// 5. Unconstrain end Y coordinate
const constrainedEndYBtn = page
.locator('[data-constraint-type="yAbsolute"][data-is-constrained="true"]')
.nth(1)
await expect(constrainedEndYBtn).toBeVisible()
await constrainedEndYBtn.click()
// Verify the constraint was removed
await editor.expectEditor.toContain('endAbsolute = [xAbs002, 84.07]', {
shouldNormalise: true,
})
// 6. Unconstrain end X coordinate
const constrainedEndXBtn = page
.locator('[data-constraint-type="xAbsolute"][data-is-constrained="true"]')
.nth(1)
await expect(constrainedEndXBtn).toBeVisible()
await constrainedEndXBtn.click()
// Verify the constraint was removed
await editor.expectEditor.toContain('endAbsolute = [237.05, 84.07]', {
shouldNormalise: true,
})
// 7. Unconstrain interior Y coordinate
const constrainedInteriorYBtn = page
.locator('[data-constraint-type="yAbsolute"][data-is-constrained="true"]')
.nth(0)
await expect(constrainedInteriorYBtn).toBeVisible()
await constrainedInteriorYBtn.click()
// Verify the constraint was removed
await editor.expectEditor.toContain(
'interiorAbsolute = [xAbs001, 100.58]',
{
shouldNormalise: true,
}
)
// 8. Unconstrain interior X coordinate
const constrainedInteriorXBtn = page
.locator('[data-constraint-type="xAbsolute"][data-is-constrained="true"]')
.nth(0)
await expect(constrainedInteriorXBtn).toBeVisible()
await constrainedInteriorXBtn.click()
// Verify all constraints were removed
await editor.expectEditor.toContain(
'interiorAbsolute = [159.26, 100.58], endAbsolute = [237.05, 84.07]',
{
shouldNormalise: true,
}
)
})
}) })

View File

@ -4,6 +4,10 @@ import toast from 'react-hot-toast'
import type { Node } from '@rust/kcl-lib/bindings/Node' import type { Node } from '@rust/kcl-lib/bindings/Node'
// Helper function to check if overlays should always be shown
const shouldAlwaysShowOverlays = () =>
localStorage.getItem('showAllOverlays') === 'true'
import type { ReactCameraProperties } from '@src/clientSideScene/CameraControls' import type { ReactCameraProperties } from '@src/clientSideScene/CameraControls'
import { import {
EXTRA_SEGMENT_HANDLE, EXTRA_SEGMENT_HANDLE,
@ -172,19 +176,28 @@ export const ClientSideScene = ({
const Overlays = () => { const Overlays = () => {
const { context } = useModelingContext() const { context } = useModelingContext()
if (context.mouseState.type === 'isDragging') return null if (context.mouseState.type === 'isDragging') return null
// Simple check directly from localStorage
const alwaysShowOverlays = shouldAlwaysShowOverlays()
// Set a large zIndex, the overlay for hover dropdown menu on line segments needs to render // Set a large zIndex, the overlay for hover dropdown menu on line segments needs to render
// over the length labels on the line segments // over the length labels on the line segments
return ( return (
<div className="absolute inset-0 pointer-events-none z-sketchOverlayDropdown"> <div className="absolute inset-0 pointer-events-none z-sketchOverlayDropdown">
{Object.entries(context.segmentOverlays) {Object.entries(context.segmentOverlays)
.flatMap((a) => .flatMap(([pathToNodeString, overlays]) =>
a[1].map((b) => ({ pathToNodeString: a[0], overlay: b })) overlays.map((b) => ({ pathToNodeString, overlay: b }))
) )
.filter((a) => a.overlay.visible) .filter((a) => alwaysShowOverlays || a.overlay.visible)
.map(({ pathToNodeString, overlay }, index) => { .map(({ pathToNodeString, overlay }, index) => {
// Force visibility if alwaysShowOverlays is true
const modifiedOverlay = alwaysShowOverlays
? { ...overlay, visible: true }
: overlay
return ( return (
<Overlay <Overlay
overlay={overlay} overlay={modifiedOverlay}
key={pathToNodeString + String(index)} key={pathToNodeString + String(index)}
pathToNodeString={pathToNodeString} pathToNodeString={pathToNodeString}
overlayIndex={index} overlayIndex={index}
@ -205,6 +218,10 @@ const Overlay = ({
pathToNodeString: string pathToNodeString: string
}) => { }) => {
const { context, send, state } = useModelingContext() const { context, send, state } = useModelingContext()
// Simple check directly from localStorage
const alwaysShowOverlays = shouldAlwaysShowOverlays()
let xAlignment = overlay.angle < 0 ? '0%' : '-100%' let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%' let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
@ -241,8 +258,9 @@ const Overlay = ({
Math.sin(((overlay.angle + offsetAngle) * Math.PI) / 180) * offset Math.sin(((overlay.angle + offsetAngle) * Math.PI) / 180) * offset
const shouldShow = const shouldShow =
overlay.visible && (overlay.visible || alwaysShowOverlays) &&
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number' && (alwaysShowOverlays ||
typeof context?.segmentHoverMap?.[pathToNodeString] === 'number') &&
!( !(
state.matches({ Sketch: 'Line tool' }) || state.matches({ Sketch: 'Line tool' }) ||
state.matches({ Sketch: 'Tangential arc to' }) || state.matches({ Sketch: 'Tangential arc to' }) ||

View File

@ -171,14 +171,14 @@ export class SceneInfra {
_overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) { _overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) {
const segmentOverlayPayload: SegmentOverlayPayload = { const segmentOverlayPayload: SegmentOverlayPayload = {
type: 'add-many', type: 'set-many',
overlays: {}, overlays: {},
} }
callbacks.forEach((cb) => { callbacks.forEach((cb) => {
const overlay = cb() const overlay = cb()
if (overlay?.type === 'set-one') { if (overlay?.type === 'set-one') {
segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg segmentOverlayPayload.overlays[overlay.pathToNodeString] = overlay.seg
} else if (overlay?.type === 'add-many') { } else if (overlay?.type === 'set-many') {
Object.assign(segmentOverlayPayload.overlays, overlay.overlays) Object.assign(segmentOverlayPayload.overlays, overlay.overlays)
} }
}) })

View File

@ -91,6 +91,7 @@ import type {
SegmentOverlays, SegmentOverlays,
} from '@src/machines/modelingMachine' } from '@src/machines/modelingMachine'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
const ANGLE_INDICATOR_RADIUS = 30 // in px const ANGLE_INDICATOR_RADIUS = 30 // in px
interface CreateSegmentArgs { interface CreateSegmentArgs {
@ -1551,10 +1552,10 @@ class ThreePointArcSegment implements SegmentUtils {
overlayDetails.forEach((payload, index) => { overlayDetails.forEach((payload, index) => {
if (payload?.type === 'set-one') { if (payload?.type === 'set-one') {
overlays[payload.pathToNodeString] = payload.seg overlays[payload.pathToNodeString] = payload.seg
// Add filterValue: 'interiorAbsolute' for p2 and 'end' for p3 // Add filterValue: 'interiorAbsolute' for p2 and 'endAbsolute' for p3
segmentOverlays.push({ segmentOverlays.push({
...payload.seg[0], ...payload.seg[0],
filterValue: index === 0 ? 'interiorAbsolute' : 'end', filterValue: index === 0 ? ARG_INTERIOR_ABSOLUTE : 'endAbsolute',
}) })
} }
}) })

View File

@ -137,7 +137,8 @@ const Loading = ({ children, className, dataTestId }: LoadingProps) => {
__html: Marked.parse( __html: Marked.parse(
CONNECTION_ERROR_TEXT[error.error] + CONNECTION_ERROR_TEXT[error.error] +
(error.context (error.context
? '\n\nThe error details are: ' + error.context ? '\n\nThe error details are: ' +
JSON.stringify(error.context)
: ''), : ''),
{ {
renderer: new SafeRenderer(markedOptions), renderer: new SafeRenderer(markedOptions),

View File

@ -267,9 +267,8 @@ export const ModelingMachineProvider = ({
'Set Segment Overlays': assign({ 'Set Segment Overlays': assign({
segmentOverlays: ({ context: { segmentOverlays }, event }) => { segmentOverlays: ({ context: { segmentOverlays }, event }) => {
if (event.type !== 'Set Segment Overlays') return {} if (event.type !== 'Set Segment Overlays') return {}
if (event.data.type === 'add-many') if (event.data.type === 'set-many')
return { return {
...segmentOverlays,
...event.data.overlays, ...event.data.overlays,
} }
if (event.data.type === 'set-one') if (event.data.type === 'set-one')
@ -1393,6 +1392,8 @@ export const ModelingMachineProvider = ({
}) })
) )
) )
result.exprInsertIndex = data.namedValue.insertIndex
if ( if (
trap(parseResultAfterInsertion) || trap(parseResultAfterInsertion) ||
!resultIsOk(parseResultAfterInsertion) !resultIsOk(parseResultAfterInsertion)
@ -1401,7 +1402,7 @@ export const ModelingMachineProvider = ({
result = { result = {
modifiedAst: parseResultAfterInsertion.program, modifiedAst: parseResultAfterInsertion.program,
pathToReplaced: astAfterReplacement.pathToReplaced, pathToReplaced: astAfterReplacement.pathToReplaced,
exprInsertIndex: astAfterReplacement.exprInsertIndex, exprInsertIndex: result.exprInsertIndex,
} }
} else if ('valueText' in data.namedValue) { } else if ('valueText' in data.namedValue) {
// If they didn't provide a constant name, // If they didn't provide a constant name,

View File

@ -24,7 +24,11 @@ import { findUsesOfTagInPipe } from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type { Artifact } from '@src/lang/std/artifactGraph' import type { Artifact } from '@src/lang/std/artifactGraph'
import { codeRefFromRange } from '@src/lang/std/artifactGraph' import { codeRefFromRange } from '@src/lang/std/artifactGraph'
import type { InputArgKeys, SimplifiedArgDetails } from '@src/lang/std/stdTypes' import type {
InputArg,
InputArgKeys,
SimplifiedArgDetails,
} from '@src/lang/std/stdTypes'
import { topLevelRange } from '@src/lang/util' import { topLevelRange } from '@src/lang/util'
import type { Identifier, Literal, LiteralValue } from '@src/lang/wasm' import type { Identifier, Literal, LiteralValue } from '@src/lang/wasm'
import { assertParse, recast } from '@src/lang/wasm' import { assertParse, recast } from '@src/lang/wasm'
@ -686,19 +690,19 @@ describe('Testing removeSingleConstraintInfo', () => {
|> /*4*/ angledLine(angle = 30 + 0, endAbsoluteY = 10.14 + 0) |> /*4*/ angledLine(angle = 30 + 0, endAbsoluteY = 10.14 + 0)
|> angledLineThatIntersects(angle = 3.14 + 0, intersectTag = a, offset = 0 + 0) |> angledLineThatIntersects(angle = 3.14 + 0, intersectTag = a, offset = 0 + 0)
|> tangentialArc(endAbsolute = [3.14 + 0, 13.14 + 0])` |> tangentialArc(endAbsolute = [3.14 + 0, 13.14 + 0])`
test.each([ const cases: [string, InputArg['type'], number | string, string][] = [
[' line(end = [3 + 0, 4])', 'arrayIndex', 1, ''], [' line(end = [3 + 0, 4])', 'arrayItem', 1, ''],
[ [
'/*0*/ angledLine(angle = 3, length = 3.14 + 0)', '/*0*/ angledLine(angle = 3, length = 3.14 + 0)',
'labeledArg', 'labeledArg',
'angle', 'angle',
'', '',
], ],
['line(endAbsolute = [6.14 + 0, 3.14 + 0])', 'arrayIndex', 0, ''], ['line(endAbsolute = [6.14 + 0, 3.14 + 0])', 'arrayItem', 0, ''],
['xLine(endAbsolute = 8)', '', '', '/*xAbs*/'], ['xLine(endAbsolute = 8)', 'singleValue', '', '/*xAbs*/'],
['yLine(endAbsolute = 5)', '', '', '/*yAbs*/'], ['yLine(endAbsolute = 5)', 'singleValue', '', '/*yAbs*/'],
['yLine(length = 3.14, tag = $a)', '', '', '/*yRel*/'], ['yLine(length = 3.14, tag = $a)', 'singleValue', '', '/*yRel*/'],
['xLine(length = 3.14)', '', '', '/*xRel*/'], ['xLine(length = 3.14)', 'singleValue', '', '/*xRel*/'],
[ [
'/*1*/ angledLine(angle = 3, lengthX = 3.14 + 0)', '/*1*/ angledLine(angle = 3, lengthX = 3.14 + 0)',
'labeledArg', 'labeledArg',
@ -731,11 +735,12 @@ describe('Testing removeSingleConstraintInfo', () => {
], ],
[ [
'tangentialArc(endAbsolute = [3.14 + 0, 13.14])', 'tangentialArc(endAbsolute = [3.14 + 0, 13.14])',
'labeledArg', 'labeledArgArrayItem',
'endAbsolute', 'endAbsolute',
'', '',
], ],
] as const)( ]
test.each(cases)(
'stdlib fn: %s', 'stdlib fn: %s',
async (expectedFinish, key, value, commentLabel) => { async (expectedFinish, key, value, commentLabel) => {
const ast = assertParse(code) const ast = assertParse(code)
@ -749,19 +754,26 @@ describe('Testing removeSingleConstraintInfo', () => {
const range = topLevelRange(start + 1, start + lineOfInterest.length) const range = topLevelRange(start + 1, start + lineOfInterest.length)
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
let argPosition: SimplifiedArgDetails let argPosition: SimplifiedArgDetails
if (key === 'arrayIndex' && typeof value === 'number') { if (key === 'arrayItem' && typeof value === 'number') {
argPosition = { argPosition = {
type: 'arrayItem', type: 'arrayItem',
index: value === 0 ? 0 : 1, index: value === 0 ? 0 : 1,
} }
} else if (key === '') { } else if (key === 'singleValue') {
argPosition = { argPosition = {
type: 'singleValue', type: 'singleValue',
} }
} else if (key === 'labeledArg') { } else if (key === 'labeledArg' && typeof value === 'string') {
argPosition = { argPosition = {
type: 'labeledArg', type: 'labeledArg',
key: value, key: value as any,
}
} else if (key === 'labeledArgArrayItem') {
console.log()
argPosition = {
type: 'labeledArgArrayItem',
key: value as any,
index: 1,
} }
} else { } else {
throw new Error('argPosition is undefined') throw new Error('argPosition is undefined')

View File

@ -1038,7 +1038,6 @@ export function updatePathToNodesAfterEdit(
if (err(oldNodeResult)) return oldNodeResult if (err(oldNodeResult)) return oldNodeResult
const oldNode = oldNodeResult.node const oldNode = oldNodeResult.node
const varName = oldNode.declaration.id.name const varName = oldNode.declaration.id.name
console.log('varName', varName)
// Find the old and new indices for this variable // Find the old and new indices for this variable
const oldIndex = oldVarDecls.get(varName) const oldIndex = oldVarDecls.get(varName)

View File

@ -571,7 +571,11 @@ describe('testing getConstraintInfo', () => {
isConstrained: false, isConstrained: false,
value: '3.14', value: '3.14',
sourceRange: [expect.any(Number), expect.any(Number), 0], sourceRange: [expect.any(Number), expect.any(Number), 0],
argPosition: { type: 'arrayItem', index: 0 }, argPosition: {
type: 'labeledArgArrayItem',
key: 'endAbsolute',
index: 0,
},
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArc', stdLibFnName: 'tangentialArc',
}, },
@ -580,7 +584,11 @@ describe('testing getConstraintInfo', () => {
isConstrained: false, isConstrained: false,
value: '13.14', value: '13.14',
sourceRange: [expect.any(Number), expect.any(Number), 0], sourceRange: [expect.any(Number), expect.any(Number), 0],
argPosition: { type: 'arrayItem', index: 1 }, argPosition: {
type: 'labeledArgArrayItem',
key: 'endAbsolute',
index: 1,
},
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArc', stdLibFnName: 'tangentialArc',
}, },
@ -1085,7 +1093,11 @@ describe('testing getConstraintInfo', () => {
isConstrained: true, isConstrained: true,
value: '3.14 + 0', value: '3.14 + 0',
sourceRange: [expect.any(Number), expect.any(Number), 0], sourceRange: [expect.any(Number), expect.any(Number), 0],
argPosition: { type: 'arrayItem', index: 0 }, argPosition: {
type: 'labeledArgArrayItem',
key: 'endAbsolute',
index: 0,
},
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArc', stdLibFnName: 'tangentialArc',
}, },
@ -1094,7 +1106,11 @@ describe('testing getConstraintInfo', () => {
isConstrained: true, isConstrained: true,
value: '13.14 + 0', value: '13.14 + 0',
sourceRange: [expect.any(Number), expect.any(Number), 0], sourceRange: [expect.any(Number), expect.any(Number), 0],
argPosition: { type: 'arrayItem', index: 1 }, argPosition: {
type: 'labeledArgArrayItem',
key: 'endAbsolute',
index: 1,
},
pathToNode: expect.any(Array), pathToNode: expect.any(Array),
stdLibFnName: 'tangentialArc', stdLibFnName: 'tangentialArc',
}, },

View File

@ -98,7 +98,7 @@ import type { EdgeCutInfo } from '@src/machines/modelingMachine'
const STRAIGHT_SEGMENT_ERR = new Error( const STRAIGHT_SEGMENT_ERR = new Error(
'Invalid input, expected "straight-segment"' 'Invalid input, expected "straight-segment"'
) )
const ARC_SEGMENT_ERR = new Error('Invalid input, expected "arc-segment"') const ARC_SEGMENT_ERR = () => new Error('Invalid input, expected "arc-segment"')
const CIRCLE_THREE_POINT_SEGMENT_ERR = new Error( const CIRCLE_THREE_POINT_SEGMENT_ERR = new Error(
'Invalid input, expected "circle-three-point-segment"' 'Invalid input, expected "circle-three-point-segment"'
) )
@ -1026,13 +1026,15 @@ export const tangentialArc: SketchLineHelperKw = {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode) const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
const result = replaceExistingCallback([ const result = replaceExistingCallback([
{ {
type: 'arrayItem', type: 'labeledArgArrayItem',
key: ARG_END_ABSOLUTE,
index: 0, index: 0,
argType: 'xAbsolute', argType: 'xAbsolute',
expr: toX, expr: toX,
}, },
{ {
type: 'arrayItem', type: 'labeledArgArrayItem',
key: ARG_END_ABSOLUTE,
index: 1, index: 1,
argType: 'yAbsolute', argType: 'yAbsolute',
expr: toY, expr: toY,
@ -1145,28 +1147,32 @@ export const tangentialArc: SketchLineHelperKw = {
['arg', LABELED_ARG_FIELD], ['arg', LABELED_ARG_FIELD],
] ]
if (expr.type !== 'ArrayExpression' || expr.elements.length < 2) { if (expr.type !== 'ArrayExpression' || expr.elements.length < 2) {
constraints.push( constraints.push({
constrainInfo( stdLibFnName: 'tangentialArc',
'xAbsolute', type: 'xAbsolute',
isNotLiteralArrayOrStatic(expr), isConstrained: isNotLiteralArrayOrStatic(expr),
code.slice(expr.start, expr.end), sourceRange: topLevelRange(expr.start, expr.end),
'tangentialArc', pathToNode: pathToArg,
0, value: code.slice(expr.start, expr.end),
topLevelRange(expr.start, expr.end), argPosition: {
pathToArg type: 'labeledArgArrayItem',
) index: 0,
) key: ARG_END_ABSOLUTE,
constraints.push( },
constrainInfo( })
'yAbsolute', constraints.push({
isNotLiteralArrayOrStatic(expr), stdLibFnName: 'tangentialArc',
code.slice(expr.start, expr.end), type: 'yAbsolute',
'tangentialArc', isConstrained: isNotLiteralArrayOrStatic(expr),
1, sourceRange: topLevelRange(expr.start, expr.end),
topLevelRange(expr.start, expr.end), pathToNode: pathToArg,
pathToArg value: code.slice(expr.start, expr.end),
) argPosition: {
) type: 'labeledArgArrayItem',
index: 1,
key: ARG_END_ABSOLUTE,
},
})
return constraints return constraints
} }
const pathToX: PathToNode = [ const pathToX: PathToNode = [
@ -1181,72 +1187,79 @@ export const tangentialArc: SketchLineHelperKw = {
] ]
const exprX = expr.elements[0] const exprX = expr.elements[0]
const exprY = expr.elements[1] const exprY = expr.elements[1]
constraints.push( constraints.push({
constrainInfo( stdLibFnName: 'tangentialArc',
'xAbsolute', type: 'xAbsolute',
isNotLiteralArrayOrStatic(exprX), isConstrained: isNotLiteralArrayOrStatic(exprX),
code.slice(exprX.start, exprX.end), sourceRange: topLevelRange(exprX.start, exprX.end),
'tangentialArc', pathToNode: pathToX,
0, value: code.slice(exprX.start, exprX.end),
topLevelRange(exprX.start, exprX.end), argPosition: {
pathToX type: 'labeledArgArrayItem',
) index: 0,
) key: ARG_END_ABSOLUTE,
constraints.push( },
constrainInfo( })
'yAbsolute', constraints.push({
isNotLiteralArrayOrStatic(exprY), stdLibFnName: 'tangentialArc',
code.slice(exprY.start, exprY.end), type: 'yAbsolute',
'tangentialArc', isConstrained: isNotLiteralArrayOrStatic(exprY),
1, sourceRange: topLevelRange(exprY.start, exprY.end),
topLevelRange(exprY.start, exprY.end), pathToNode: pathToY,
pathToY value: code.slice(exprY.start, exprY.end),
) argPosition: {
) type: 'labeledArgArrayItem',
index: 1,
key: ARG_END_ABSOLUTE,
},
})
} }
return constraints return constraints
}, },
} }
export const circle: SketchLineHelperKw = { export const circle: SketchLineHelperKw = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'arc-segment') return ARC_SEGMENT_ERR if (segmentInput.type !== 'arc-segment') return ARC_SEGMENT_ERR()
const { center, radius } = segmentInput const { center, radius } = segmentInput
const _node = { ...node } const _node = { ...node }
// Try to get the pipe expression first
const nodeMeta = getNodeFromPath<PipeExpression>( const nodeMeta = getNodeFromPath<PipeExpression>(
_node, _node,
pathToNode, pathToNode,
'PipeExpression' 'PipeExpression'
) )
if (err(nodeMeta)) return nodeMeta
// If we get a pipe expression, handle as before
if (!err(nodeMeta) && nodeMeta.node.type === 'PipeExpression') {
const { node: pipe } = nodeMeta const { node: pipe } = nodeMeta
const x = createLiteral(roundOff(center[0], 2)) const x = createLiteral(roundOff(center[0], 2))
const y = createLiteral(roundOff(center[1], 2)) const y = createLiteral(roundOff(center[1], 2))
const radiusExp = createLiteral(roundOff(radius, 2)) const radiusExp = createLiteral(roundOff(radius, 2))
const centerArray = createArrayExpression([x, y])
if (replaceExistingCallback) { if (replaceExistingCallback) {
const result = replaceExistingCallback([ const result = replaceExistingCallback([
{ {
type: 'arrayInObject', type: 'labeledArgArrayItem',
index: 0,
key: 'center',
argType: 'xAbsolute', argType: 'xAbsolute',
key: ARG_CIRCLE_CENTER,
index: 0,
expr: x, expr: x,
}, },
{ {
type: 'arrayInObject', type: 'labeledArgArrayItem',
index: 1,
key: 'center',
argType: 'yAbsolute', argType: 'yAbsolute',
key: ARG_CIRCLE_CENTER,
index: 1,
expr: y, expr: y,
}, },
{ {
type: 'objectProperty', type: 'labeledArg',
key: 'radius',
argType: 'radius', argType: 'radius',
key: ARG_RADIUS,
expr: radiusExp, expr: radiusExp,
}, },
]) ])
@ -1254,7 +1267,25 @@ export const circle: SketchLineHelperKw = {
const { callExp, valueUsedInTransform } = result const { callExp, valueUsedInTransform } = result
const { index: callIndex } = splitPathAtPipeExpression(pathToNode) const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
// Handle the case where the returned expression is not a proper kwarg expression
if (callExp.type !== 'CallExpressionKw') {
// In a pipe expression, the unlabeled first arg can be omitted
const centerArg = createLabeledArg(ARG_CIRCLE_CENTER, centerArray)
const radiusArg = createLabeledArg(ARG_RADIUS, radiusExp)
const circleKw = createCallExpressionStdLibKw('circle', null, [
centerArg,
radiusArg,
])
pipe.body[callIndex] = circleKw
} else {
// For CallExpressionKw, we don't need to set an unlabeled argument in pipe expressions
if (callExp.unlabeled) {
callExp.unlabeled = null
}
pipe.body[callIndex] = callExp pipe.body[callIndex] = callExp
}
return { return {
modifiedAst: _node, modifiedAst: _node,
@ -1262,10 +1293,109 @@ export const circle: SketchLineHelperKw = {
valueUsedInTransform, valueUsedInTransform,
} }
} }
return new Error('not implemented') return new Error('Problem with circle')
}
// If it's not in a pipe expression, try to get variable declarator
const varDecMeta = getNodeFromPath<VariableDeclarator>(
_node,
pathToNode,
'VariableDeclarator'
)
if (err(varDecMeta))
return new Error('Could not find pipe expression or variable declarator')
const { node: varDec } = varDecMeta
// Get the existing circle expression to extract the unlabeled first argument (sketch)
const existingCircleExpr = varDec.init as Node<CallExpressionKw>
let sketchArg: Expr | null = null
// Extract the unlabeled sketch argument if it exists
if (existingCircleExpr && existingCircleExpr.type === 'CallExpressionKw') {
sketchArg = existingCircleExpr.unlabeled
}
// These follow the same pattern whether we use the callback or not
const x = createLiteral(roundOff(center[0], 2))
const y = createLiteral(roundOff(center[1], 2))
const radiusExp = createLiteral(roundOff(radius, 2))
const centerArray = createArrayExpression([x, y])
if (replaceExistingCallback) {
// debugger
const result = replaceExistingCallback([
{
type: 'labeledArgArrayItem',
argType: 'xAbsolute',
key: ARG_CIRCLE_CENTER,
index: 0,
expr: x,
},
{
type: 'labeledArgArrayItem',
argType: 'yAbsolute',
key: ARG_CIRCLE_CENTER,
index: 1,
expr: y,
},
{
type: 'labeledArg',
argType: 'radius',
key: ARG_RADIUS,
expr: radiusExp,
},
])
if (err(result)) return result
const { callExp, valueUsedInTransform } = result
// Make sure the unlabeled first argument (sketch) is preserved
if (callExp.type === 'CallExpressionKw') {
if (sketchArg && !callExp.unlabeled) {
callExp.unlabeled = sketchArg
}
// Replace the variable declarator init with the call expression
varDec.init = callExp
} else {
// If somehow we get a non-kw expression, create the correct one
const centerArg = createLabeledArg(ARG_CIRCLE_CENTER, centerArray)
const radiusArg = createLabeledArg(ARG_RADIUS, radiusExp)
const circleKw = createCallExpressionStdLibKw('circle', sketchArg, [
centerArg,
radiusArg,
])
// Replace the variable declarator init with the correct KW expression
varDec.init = circleKw
}
return {
modifiedAst: _node,
pathToNode,
valueUsedInTransform,
}
} else {
// If no callback, create a CallExpressionKw directly
const centerArg = createLabeledArg(ARG_CIRCLE_CENTER, centerArray)
const radiusArg = createLabeledArg(ARG_RADIUS, radiusExp)
const circleKw = createCallExpressionStdLibKw('circle', sketchArg, [
centerArg,
radiusArg,
])
// Replace the variable declarator init with the call expression
varDec.init = circleKw
return {
modifiedAst: _node,
pathToNode,
}
}
}, },
updateArgs: ({ node, pathToNode, input }) => { updateArgs: ({ node, pathToNode, input }) => {
if (input.type !== 'arc-segment') return ARC_SEGMENT_ERR if (input.type !== 'arc-segment') return ARC_SEGMENT_ERR()
const { center, radius } = input const { center, radius } = input
const _node = { ...node } const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpressionKw>(_node, pathToNode) const nodeMeta = getNodeFromPath<CallExpressionKw>(_node, pathToNode)
@ -1301,6 +1431,7 @@ export const circle: SketchLineHelperKw = {
['arguments', 'CallExpressionKw'], ['arguments', 'CallExpressionKw'],
[centerInfo.argIndex, ARG_INDEX_FIELD], [centerInfo.argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD], ['arg', LABELED_ARG_FIELD],
['elements', 'ArrayExpression'],
] ]
const pathToRadiusLiteral: PathToNode = [ const pathToRadiusLiteral: PathToNode = [
...pathToNode, ...pathToNode,
@ -1317,16 +1448,19 @@ export const circle: SketchLineHelperKw = {
[1, 'index'], [1, 'index'],
] ]
return [ const constraints: ConstrainInfo[] = [
constrainInfo( {
'radius', stdLibFnName: 'circle',
isNotLiteralArrayOrStatic(radiusInfo.expr), type: 'radius',
code.slice(radiusInfo.expr.start, radiusInfo.expr.end), isConstrained: isNotLiteralArrayOrStatic(radiusInfo.expr),
'circle', sourceRange: topLevelRange(radiusInfo.expr.start, radiusInfo.expr.end),
'radius', pathToNode: pathToRadiusLiteral,
topLevelRange(radiusInfo.expr.start, radiusInfo.expr.end), value: code.slice(radiusInfo.expr.start, radiusInfo.expr.end),
pathToRadiusLiteral argPosition: {
), type: 'labeledArg',
key: ARG_RADIUS,
},
},
{ {
stdLibFnName: 'circle', stdLibFnName: 'circle',
type: 'xAbsolute', type: 'xAbsolute',
@ -1341,9 +1475,9 @@ export const circle: SketchLineHelperKw = {
centerInfo.expr.elements[0].end centerInfo.expr.elements[0].end
), ),
argPosition: { argPosition: {
type: 'arrayInObject', type: 'labeledArgArrayItem',
index: 0, index: 0,
key: 'center', key: ARG_CIRCLE_CENTER,
}, },
}, },
{ {
@ -1360,12 +1494,13 @@ export const circle: SketchLineHelperKw = {
centerInfo.expr.elements[1].end centerInfo.expr.elements[1].end
), ),
argPosition: { argPosition: {
type: 'arrayInObject', type: 'labeledArgArrayItem',
index: 1, index: 1,
key: 'center', key: 'center',
}, },
}, },
] ]
return constraints
}, },
} }
@ -1378,7 +1513,7 @@ export const arc: SketchLineHelperKw = {
replaceExistingCallback, replaceExistingCallback,
spliceBetween, spliceBetween,
}) => { }) => {
if (segmentInput.type !== 'arc-segment') return ARC_SEGMENT_ERR if (segmentInput.type !== 'arc-segment') return ARC_SEGMENT_ERR()
const { center, radius, from, to } = segmentInput const { center, radius, from, to } = segmentInput
const _node = { ...node } const _node = { ...node }
@ -1512,7 +1647,7 @@ export const arc: SketchLineHelperKw = {
} }
}, },
updateArgs: ({ node, pathToNode, input }) => { updateArgs: ({ node, pathToNode, input }) => {
if (input.type !== 'arc-segment') return ARC_SEGMENT_ERR if (input.type !== 'arc-segment') return ARC_SEGMENT_ERR()
const { center, radius, from, to } = input const { center, radius, from, to } = input
const _node = { ...node } const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpressionKw>(_node, pathToNode) const nodeMeta = getNodeFromPath<CallExpressionKw>(_node, pathToNode)
@ -1653,7 +1788,7 @@ export const arcTo: SketchLineHelperKw = {
spliceBetween, spliceBetween,
}) => { }) => {
if (segmentInput.type !== 'circle-three-point-segment') if (segmentInput.type !== 'circle-three-point-segment')
return ARC_SEGMENT_ERR return ARC_SEGMENT_ERR()
const { p2, p3 } = segmentInput const { p2, p3 } = segmentInput
const _node = { ...node } const _node = { ...node }
@ -1669,29 +1804,43 @@ export const arcTo: SketchLineHelperKw = {
// p1 is the start point (from the previous segment) // p1 is the start point (from the previous segment)
// p2 is the interiorAbsolute point // p2 is the interiorAbsolute point
// p3 is the end point // p3 is the end point
const interiorAbsolute = createArrayExpression([ const p2x = createLiteral(roundOff(p2[0], 2))
createLiteral(roundOff(p2[0], 2)), const p2y = createLiteral(roundOff(p2[1], 2))
createLiteral(roundOff(p2[1], 2)), const interiorAbsolute = createArrayExpression([p2x, p2y])
])
const end = createArrayExpression([ const p3x = createLiteral(roundOff(p3[0], 2))
createLiteral(roundOff(p3[0], 2)), const p3y = createLiteral(roundOff(p3[1], 2))
createLiteral(roundOff(p3[1], 2)), const end = createArrayExpression([p3x, p3y])
])
if (replaceExistingCallback) { if (replaceExistingCallback) {
const result = replaceExistingCallback([ const result = replaceExistingCallback([
{ {
type: 'objectProperty', type: 'labeledArgArrayItem',
key: 'interiorAbsolute', key: 'interiorAbsolute',
index: 0,
argType: 'xAbsolute', argType: 'xAbsolute',
expr: createLiteral(0), // This is a workaround, the actual value will be set later expr: p2x,
}, },
{ {
type: 'objectProperty', type: 'labeledArgArrayItem',
key: 'endAbsolute', key: 'interiorAbsolute',
index: 1,
argType: 'yAbsolute', argType: 'yAbsolute',
expr: createLiteral(0), // This is a workaround, the actual value will be set later expr: p2y,
},
{
type: 'labeledArgArrayItem',
key: 'endAbsolute',
index: 0,
argType: 'xAbsolute',
expr: p3x,
},
{
type: 'labeledArgArrayItem',
key: 'endAbsolute',
index: 1,
argType: 'yAbsolute',
expr: p3y,
}, },
]) ])
if (err(result)) return result if (err(result)) return result
@ -1757,7 +1906,7 @@ export const arcTo: SketchLineHelperKw = {
} }
}, },
updateArgs: ({ node, pathToNode, input }) => { updateArgs: ({ node, pathToNode, input }) => {
if (input.type !== 'circle-three-point-segment') return ARC_SEGMENT_ERR if (input.type !== 'circle-three-point-segment') return ARC_SEGMENT_ERR()
const { p2, p3 } = input const { p2, p3 } = input
const _node = { ...node } const _node = { ...node }
@ -1866,8 +2015,8 @@ export const arcTo: SketchLineHelperKw = {
), ),
stdLibFnName: 'arc', stdLibFnName: 'arc',
argPosition: { argPosition: {
type: 'arrayInObject', type: 'labeledArgArrayItem',
key: 'interiorAbsolute', key: ARG_INTERIOR_ABSOLUTE,
index: 0, index: 0,
}, },
sourceRange: topLevelRange( sourceRange: topLevelRange(
@ -1875,7 +2024,7 @@ export const arcTo: SketchLineHelperKw = {
interiorAbsoluteArr.elements[0].end interiorAbsoluteArr.elements[0].end
), ),
pathToNode: pathToInteriorX, pathToNode: pathToInteriorX,
filterValue: 'interiorAbsolute', filterValue: ARG_INTERIOR_ABSOLUTE,
}, },
{ {
type: 'yAbsolute', type: 'yAbsolute',
@ -1888,8 +2037,8 @@ export const arcTo: SketchLineHelperKw = {
), ),
stdLibFnName: 'arc', stdLibFnName: 'arc',
argPosition: { argPosition: {
type: 'arrayInObject', type: 'labeledArgArrayItem',
key: 'interiorAbsolute', key: ARG_INTERIOR_ABSOLUTE,
index: 1, index: 1,
}, },
sourceRange: topLevelRange( sourceRange: topLevelRange(
@ -1897,7 +2046,7 @@ export const arcTo: SketchLineHelperKw = {
interiorAbsoluteArr.elements[1].end interiorAbsoluteArr.elements[1].end
), ),
pathToNode: pathToInteriorY, pathToNode: pathToInteriorY,
filterValue: 'interiorAbsolute', filterValue: ARG_INTERIOR_ABSOLUTE,
}, },
{ {
type: 'xAbsolute', type: 'xAbsolute',
@ -1905,8 +2054,8 @@ export const arcTo: SketchLineHelperKw = {
value: code.slice(endArr.elements[0].start, endArr.elements[0].end), value: code.slice(endArr.elements[0].start, endArr.elements[0].end),
stdLibFnName: 'arc', stdLibFnName: 'arc',
argPosition: { argPosition: {
type: 'arrayInObject', type: 'labeledArgArrayItem',
key: 'end', key: 'endAbsolute',
index: 0, index: 0,
}, },
sourceRange: topLevelRange( sourceRange: topLevelRange(
@ -1914,7 +2063,7 @@ export const arcTo: SketchLineHelperKw = {
endArr.elements[0].end endArr.elements[0].end
), ),
pathToNode: pathToEndX, pathToNode: pathToEndX,
filterValue: 'end', filterValue: 'endAbsolute',
}, },
{ {
type: 'yAbsolute', type: 'yAbsolute',
@ -1922,8 +2071,8 @@ export const arcTo: SketchLineHelperKw = {
value: code.slice(endArr.elements[1].start, endArr.elements[1].end), value: code.slice(endArr.elements[1].start, endArr.elements[1].end),
stdLibFnName: 'arc', stdLibFnName: 'arc',
argPosition: { argPosition: {
type: 'arrayInObject', type: 'labeledArgArrayItem',
key: 'end', key: 'endAbsolute',
index: 1, index: 1,
}, },
sourceRange: topLevelRange( sourceRange: topLevelRange(
@ -1931,7 +2080,7 @@ export const arcTo: SketchLineHelperKw = {
endArr.elements[1].end endArr.elements[1].end
), ),
pathToNode: pathToEndY, pathToNode: pathToEndY,
filterValue: 'end', filterValue: 'endAbsolute',
}, },
] ]
@ -3052,6 +3201,54 @@ export function changeSketchArguments(
return new Error(`not a sketch line helper: ${fnName}`) return new Error(`not a sketch line helper: ${fnName}`)
} }
/**
* Converts a function name to a ToolTip (UI hint/identifier) based on the segment type.
*
* This function differs from fnNameToTooltip() in that it uses the Path/segment
* type information to determine the correct ToolTip, rather than analyzing function
* argument labels. This is particularly important for functions like 'arc' where
* the same function name can map to different ToolTips ('arc' or 'arcTo') depending
* on the segment type (ArcThreePoint vs other types).
*
* While fnNameToTooltip() determines the ToolTip by examining the function's argument
* structure at call site, this function uses the segment geometry information, making
* it suitable for contexts where we have the Path object but not the full argument list.
*
* @param seg - The Path object containing segment type information
* @param fnName - The function name to convert to a ToolTip
* @returns The corresponding ToolTip or an Error if the function name is unknown
*/
export function fnNameToToolTipFromSegment(
seg: Path,
fnName: string
): ToolTip | Error {
switch (fnName) {
case 'arc': {
return seg.type === 'ArcThreePoint' ? 'arcTo' : 'arc'
}
case 'line':
case 'lineTo':
case 'xLine':
case 'xLineTo':
case 'yLine':
case 'yLineTo':
case 'angledLineToX':
case 'angledLineToY':
case 'angledLineOfXLength':
case 'angledLineOfYLength':
case 'angledLineThatIntersects':
case 'circleThreePoint':
case 'circle':
case 'tangentialArc':
case 'angledLine':
return fnName
default:
const err = `Unknown sketch line function ${fnName}`
console.error(err)
return new Error(err)
}
}
/** /**
* Function names no longer cleanly correspond to tooltips. * Function names no longer cleanly correspond to tooltips.
* A tooltip is a user action, like a line to a given point, or in a given direction. * A tooltip is a user action, like a line to a given point, or in a given direction.
@ -3770,18 +3967,30 @@ export const getArc = (
callExp: CallExpressionKw callExp: CallExpressionKw
): ):
| { | {
val: [Expr, Expr, Expr] val: [Expr, Expr, Expr] | [Expr, Expr]
tag?: Expr tag?: Expr
} }
| Error => { | Error => {
const angleStart = findKwArg(ARG_ANGLE_START, callExp) const angleStart = findKwArg(ARG_ANGLE_START, callExp)
const angleEnd = findKwArg(ARG_ANGLE_END, callExp) const angleEnd = findKwArg(ARG_ANGLE_END, callExp)
const radius = findKwArg(ARG_RADIUS, callExp) const radius = findKwArg(ARG_RADIUS, callExp)
if (!angleStart || !angleEnd || !radius) { const isMissingAnyAngleKwArgs = !angleStart || !angleEnd || !radius
return new Error(`arc call needs angleStart, angleEnd, and radius args`)
} const interiorAbsolute = findKwArg(ARG_INTERIOR_ABSOLUTE, callExp)
const endAbsolute = findKwArg(ARG_END_ABSOLUTE, callExp)
const isMissingAnyEndKwArgs = !interiorAbsolute || !endAbsolute
if (!isMissingAnyAngleKwArgs) {
const tag = findKwArg(ARG_TAG, callExp) const tag = findKwArg(ARG_TAG, callExp)
return { val: [angleStart, angleEnd, radius], tag } return { val: [angleStart, angleEnd, radius], tag }
} else if (!isMissingAnyEndKwArgs) {
const tag = findKwArg(ARG_TAG, callExp)
return { val: [interiorAbsolute, endAbsolute], tag }
}
return new Error(
`arc call needs [angleStart, angleEnd, radius] or [interiorAbsolute, endAbsolute] args`
)
} }
/** /**

View File

@ -39,6 +39,7 @@ import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import { import {
createFirstArg, createFirstArg,
fnNameToTooltip, fnNameToTooltip,
fnNameToToolTipFromSegment,
getAngledLine, getAngledLine,
getAngledLineThatIntersects, getAngledLineThatIntersects,
getArc, getArc,
@ -79,7 +80,12 @@ import type {
} from '@src/lang/wasm' } from '@src/lang/wasm'
import { sketchFromKclValue } from '@src/lang/wasm' import { sketchFromKclValue } from '@src/lang/wasm'
import type { Selections } from '@src/lib/selections' import type { Selections } from '@src/lib/selections'
import { cleanErrs, err, isErr, isNotErr } from '@src/lib/trap' import {
cleanErrs,
err,
isErr as _isErr,
isNotErr as _isNotErr,
} from '@src/lib/trap'
import { import {
allLabels, allLabels,
getAngle, getAngle,
@ -1420,40 +1426,114 @@ export function removeSingleConstraint({
'This code path only works with callExpressionKw but a positional call was somehow passed' 'This code path only works with callExpressionKw but a positional call was somehow passed'
) )
} }
// Get all current labeled arguments from the CallExpressionKw
const existingArgs = callExp.node.arguments
const toReplace = inputToReplace.key const toReplace = inputToReplace.key
let argsPreFilter = inputs.map((arg) => {
if (arg.type !== 'labeledArg') { // Basic approach: get the current args, filter out the TAG arg if it exists,
return new Error(`arg isn't a labeled arg: ${arg.type}`) // replace the targetArg with the raw value
}
const k = arg.key // 1. Filter out any existing tag argument since it will be handled separately
if (k !== toReplace) { const filteredArgs = existingArgs.filter(
return createLabeledArg(k, arg.expr) (arg) => arg.label.name !== ARG_TAG
} else { )
// 2. Map through the args, replacing only the one we want to change
const labeledArgs = filteredArgs.map((arg) => {
if (arg.label.name === toReplace) {
// Find the raw value to use for the argument being replaced
const rawArgVersion = rawArgs.find( const rawArgVersion = rawArgs.find(
(a) => a.type === 'labeledArg' && a.key === k (a) => a.type === 'labeledArg' && a.key === toReplace
) )
if (!rawArgVersion) { if (!rawArgVersion) {
return new Error( console.error(`Raw arg version not found for key: ${toReplace}`)
`raw arg version not found while trying to remove constraint: ${JSON.stringify(arg)}` // If raw value not found, preserve the original argument
) return arg
} }
return createLabeledArg(k, rawArgVersion.expr)
// Replace with raw value
return createLabeledArg(toReplace, rawArgVersion.expr)
} }
// Keep other arguments as they are
return arg
}) })
const args = argsPreFilter.filter(isNotErr)
const errorArgs = argsPreFilter.filter(isErr)
if (errorArgs.length > 0) {
return new Error('Error while trying to remove constraint', {
cause: errorArgs,
})
}
const noncode = callExp.node.nonCodeMeta const noncode = callExp.node.nonCodeMeta
// Use the existing unlabeled argument if available, otherwise use undefined
const unlabeledArg = callExp.node.unlabeled ?? undefined
return createStdlibCallExpressionKw( return createStdlibCallExpressionKw(
callExp.node.callee.name.name as ToolTip, callExp.node.callee.name.name as ToolTip,
args, labeledArgs,
tag, tag,
undefined, undefined,
unlabeledArg,
noncode
)
}
if (inputToReplace.type === 'labeledArgArrayItem') {
if (callExp.node.type !== 'CallExpressionKw') {
return new Error(
'This code path only works with callExpressionKw but a positional call was somehow passed'
)
}
// Get all current labeled arguments from the CallExpressionKw
const existingArgs = callExp.node.arguments
const targetKey = inputToReplace.key
const targetIndex = inputToReplace.index
// Create a copy of the existing labeled arguments
const labeledArgs = existingArgs.map((arg) => {
// Only modify the specific argument that matches the targeted key
if (
arg.label.name === targetKey &&
arg.arg.type === 'ArrayExpression'
) {
// We're dealing with an array expression within a labeled argument
const arrayElements = [...arg.arg.elements]
// Find the raw value to use for the argument item being replaced
const rawArgVersion = rawArgs.find(
(a) =>
a.type === 'labeledArgArrayItem' &&
a.key === targetKey &&
a.index === targetIndex
)
if (rawArgVersion && 'expr' in rawArgVersion) {
// Replace just the specific array element with the raw value
arrayElements[targetIndex] = rawArgVersion.expr
// Create a new labeled argument with the modified array
return createLabeledArg(
targetKey,
createArrayExpression(arrayElements)
)
}
// If no raw value found, keep the original argument
return arg
}
// Return other arguments unchanged
return arg
})
const noncode = callExp.node.nonCodeMeta
// Use the existing unlabeled argument if available, otherwise use undefined
const unlabeledArg = callExp.node.unlabeled ?? undefined
return createStdlibCallExpressionKw(
callExp.node.callee.name.name as ToolTip,
labeledArgs,
tag,
undefined, undefined,
unlabeledArg,
noncode noncode
) )
} }
@ -2057,6 +2137,15 @@ export function transformAstSketchLines({
argType: a.type, argType: a.type,
}) })
break break
case 'labeledArgArrayItem':
inputs.push({
type: 'labeledArgArrayItem',
key: a.argPosition.key,
index: a.argPosition.index,
expr: nodeMeta.node,
argType: a.type,
})
break
case 'arrayInObject': case 'arrayInObject':
inputs.push({ inputs.push({
type: 'arrayInObject', type: 'arrayInObject',
@ -2105,13 +2194,9 @@ export function transformAstSketchLines({
} }
const { to, from } = seg const { to, from } = seg
// Note to ADAM: Here is where the replaceExisting call gets sent. // Note to ADAM: Here is where the replaceExisting call gets sent.
const replacedSketchLine = replaceSketchLine({ const segmentInput: Parameters<
node: node, typeof replaceSketchLine
variables: memVars, >[0]['segmentInput'] =
pathToNode: _pathToNode,
referencedSegment,
fnName: transformTo || (call.node.callee.name.name as ToolTip),
segmentInput:
seg.type === 'Circle' seg.type === 'Circle'
? { ? {
type: 'arc-segment', type: 'arc-segment',
@ -2132,8 +2217,19 @@ export function transformAstSketchLines({
type: 'straight-segment', type: 'straight-segment',
to, to,
from, from,
}, }
const fnName = fnNameToToolTipFromSegment(
seg,
transformTo || (call.node.callee.name.name as ToolTip)
)
if (err(fnName)) return fnName
const replacedSketchLine = replaceSketchLine({
node: node,
variables: memVars,
pathToNode: _pathToNode,
referencedSegment,
fnName,
segmentInput,
replaceExistingCallback: (rawArgs) => replaceExistingCallback: (rawArgs) =>
callBack({ callBack({
referenceSegName: _referencedSegmentName, referenceSegName: _referencedSegmentName,

View File

@ -171,6 +171,14 @@ interface LabeledArg<T> {
expr: T expr: T
overrideExpr?: Node<Expr> overrideExpr?: Node<Expr>
} }
interface LabeledArgArrayItem<T> {
type: 'labeledArgArrayItem'
key: InputArgKeys
index: 0 | 1
argType: LineInputsType
expr: T
overrideExpr?: Node<Expr>
}
type _InputArg<T> = type _InputArg<T> =
| SingleValueInput<T> | SingleValueInput<T>
@ -179,6 +187,7 @@ type _InputArg<T> =
| ArrayOrObjItemInput<T> | ArrayOrObjItemInput<T>
| ArrayInObject<T> | ArrayInObject<T>
| LabeledArg<T> | LabeledArg<T>
| LabeledArgArrayItem<T>
/** /**
* {@link RawArg.expr} is the current expression for each of the args for a segment * {@link RawArg.expr} is the current expression for each of the args for a segment
@ -222,6 +231,7 @@ export type SimplifiedArgDetails =
| Omit<ArrayOrObjItemInput<null>, 'expr' | 'argType'> | Omit<ArrayOrObjItemInput<null>, 'expr' | 'argType'>
| Omit<ArrayInObject<null>, 'expr' | 'argType'> | Omit<ArrayInObject<null>, 'expr' | 'argType'>
| Omit<LabeledArg<null>, 'expr' | 'argType'> | Omit<LabeledArg<null>, 'expr' | 'argType'>
| Omit<LabeledArgArrayItem<null>, 'expr' | 'argType'>
/** /**
* Represents the result of creating a sketch expression (line, tangentialArc, angledLine, circle, etc.). * Represents the result of creating a sketch expression (line, tangentialArc, angledLine, circle, etc.).

View File

@ -273,7 +273,7 @@ export type SegmentOverlayPayload =
} }
| { type: 'clear' } | { type: 'clear' }
| { | {
type: 'add-many' type: 'set-many'
overlays: SegmentOverlays overlays: SegmentOverlays
} }