Prevent double-click to constrain length on unsupported lines (#5938)

* WIP: Prevent length constraint creation on endAbsolute lines
Fixes #5937

* Typo

Thanks @franknoirot

Co-authored-by: Frank Noirot <frank@zoo.dev>

* length constraint stuff from @lrev-Dev

* Clean up

* Lint

* Add regression test for double click after sketch constraint

---------

Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
This commit is contained in:
Pierre Jacquier
2025-04-02 10:52:36 -04:00
committed by GitHub
parent d168ef94e9
commit 10c1f3a849
7 changed files with 116 additions and 65 deletions

View File

@ -1098,7 +1098,7 @@ test.describe('Electron constraint tests', () => {
test(
'Able to double click label to set constraint',
{ tag: '@electron' },
async ({ page, context, homePage, scene, editor, toolbar }) => {
async ({ page, context, homePage, scene, editor, toolbar, cmdBar }) => {
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'test-sample')
await fsp.mkdir(bracketDir, { recursive: true })
@ -1132,6 +1132,14 @@ test.describe('Electron constraint tests', () => {
await scene.waitForExecutionDone()
})
async function clickOnFirstSegmentLabel() {
const child = page
.locator('.segment-length-label-text')
.first()
.locator('xpath=..')
await child.dblclick()
}
await test.step('Double click to constrain', async () => {
// Enter sketch edit mode via feature tree
await toolbar.openPane('feature-tree')
@ -1139,21 +1147,19 @@ test.describe('Electron constraint tests', () => {
await op.dblclick()
await toolbar.closePane('feature-tree')
const child = page
.locator('.segment-length-label-text')
.first()
.locator('xpath=..')
await child.dblclick()
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
await cmdBarSubmitButton.click()
await expect(page.locator('.cm-content')).toContainText(
'length001 = 15.3'
)
await expect(page.locator('.cm-content')).toContainText(
'|> angledLine([9, length001], %)'
)
await clickOnFirstSegmentLabel()
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain('length001 = 15.3')
await editor.expectEditor.toContain('|> angledLine([9, length001], %)')
})
await test.step('Double click again and expect failure', async () => {
await clickOnFirstSegmentLabel()
await expect(
page.getByText('Unable to constrain the length of this segment')
).toBeVisible()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
})
}

View File

@ -72,6 +72,7 @@ import {
SEGMENT_LENGTH_LABEL_OFFSET_PX,
SEGMENT_LENGTH_LABEL_TEXT,
} from '@src/clientSideScene/sceneInfra'
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
import type { Coords2d } from '@src/lang/std/sketch'
import type { SegmentInputs } from '@src/lang/std/stdTypes'
import type { PathToNode } from '@src/lang/wasm'
@ -88,6 +89,7 @@ import type {
SegmentOverlayPayload,
SegmentOverlays,
} from '@src/machines/modelingMachine'
import toast from 'react-hot-toast'
const ANGLE_INDICATOR_RADIUS = 30 // in px
interface CreateSegmentArgs {
@ -1710,6 +1712,7 @@ function createLengthIndicator({
console.error('Unable to dimension segment when clicking the label.')
return
}
sceneInfra.modelingSend({
type: 'Set selection',
data: {
@ -1718,6 +1721,20 @@ function createLengthIndicator({
},
})
const canConstrainLength = angleLengthInfo({
selectionRanges: {
...selection,
graphSelections: [selection.graphSelections[0]],
},
angleOrLength: 'setLength',
})
if (err(canConstrainLength) || !canConstrainLength.enabled) {
toast.error(
'Unable to constrain the length of this segment. Check the KCL code'
)
return
}
// Command Bar
commandBarActor.send({
type: 'Find and select command',

View File

@ -0,0 +1,57 @@
import { toolTips } from '@src/lang/langHelpers'
import { getNodeFromPath } from '@src/lang/queryAst'
import { getTransformInfos } from '@src/lang/std/sketchcombos'
import type { TransformInfo } from '@src/lang/std/stdTypes'
import type { Expr } from '@src/lang/wasm'
import type { Selections } from '@src/lib/selections'
import { kclManager } from '@src/lib/singletons'
import { err } from '@src/lib/trap'
export function angleLengthInfo({
selectionRanges,
angleOrLength = 'setLength',
}: {
selectionRanges: Selections
angleOrLength?: 'setLength' | 'setAngle'
}):
| {
transforms: TransformInfo[]
enabled: boolean
}
| Error {
const nodes = selectionRanges.graphSelections.map(({ codeRef }) =>
getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode, [
'CallExpression',
'CallExpressionKw',
])
)
const _err1 = nodes.find(err)
if (_err1 instanceof Error) return _err1
const isAllTooltips = nodes.every((meta) => {
if (err(meta)) return false
return (
(meta.node?.type === 'CallExpressionKw' ||
meta.node?.type === 'CallExpression') &&
toolTips.includes(meta.node.callee.name.name as any)
)
})
const transforms = getTransformInfos(
selectionRanges,
kclManager.ast,
angleOrLength
)
const enabled =
selectionRanges.graphSelections.length <= 1 &&
isAllTooltips &&
transforms.every(Boolean)
console.log(
'enabled',
enabled,
selectionRanges.graphSelections.length,
isAllTooltips,
transforms.every(Boolean)
)
return { enabled, transforms }
}

View File

@ -3,21 +3,18 @@ import {
SetAngleLengthModal,
createSetAngleLengthModal,
} from '@src/components/SetAngleLengthModal'
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
import {
createBinaryExpressionWithUnary,
createLocalName,
createName,
createVariableDeclaration,
} from '@src/lang/create'
import { toolTips } from '@src/lang/langHelpers'
import { getNodeFromPath } from '@src/lang/queryAst'
import type { PathToNodeMap } from '@src/lang/std/sketchcombos'
import {
getTransformInfos,
isExprBinaryPart,
transformAstSketchLines,
} from '@src/lang/std/sketchcombos'
import type { TransformInfo } from '@src/lang/std/stdTypes'
import type { Expr, Program } from '@src/lang/wasm'
import type { KclCommandValue } from '@src/lib/commandTypes'
import type { Selections } from '@src/lib/selections'
@ -27,48 +24,6 @@ import { normaliseAngle } from '@src/lib/utils'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
export function angleLengthInfo({
selectionRanges,
angleOrLength = 'setLength',
}: {
selectionRanges: Selections
angleOrLength?: 'setLength' | 'setAngle'
}):
| {
transforms: TransformInfo[]
enabled: boolean
}
| Error {
const nodes = selectionRanges.graphSelections.map(({ codeRef }) =>
getNodeFromPath<Expr>(kclManager.ast, codeRef.pathToNode, [
'CallExpression',
'CallExpressionKw',
])
)
const _err1 = nodes.find(err)
if (_err1 instanceof Error) return _err1
const isAllTooltips = nodes.every((meta) => {
if (err(meta)) return false
return (
(meta.node?.type === 'CallExpressionKw' ||
meta.node?.type === 'CallExpression') &&
toolTips.includes(meta.node.callee.name.name as any)
)
})
const transforms = getTransformInfos(
selectionRanges,
kclManager.ast,
angleOrLength
)
const enabled =
selectionRanges.graphSelections.length <= 1 &&
isAllTooltips &&
transforms.every(Boolean)
return { enabled, transforms }
}
export async function applyConstraintLength({
length,
selectionRanges,

View File

@ -1,7 +1,7 @@
import type { Models } from '@kittycad/lib'
import { DEV } from '@src/env'
import { angleLengthInfo } from '@src/components/Toolbar/setAngleLength'
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
import { getNodeFromPath } from '@src/lang/queryAst'
import { getVariableDeclaration } from '@src/lang/queryAst/getVariableDeclaration'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'

View File

@ -2,6 +2,7 @@ import { DEV } from '@src/env'
import type { EventFrom, StateFrom } from 'xstate'
import type { CustomIconName } from '@src/components/CustomIcon'
import { createLiteral } from '@src/lang/create'
import { commandBarActor } from '@src/machines/commandBarMachine'
import type { modelingMachine } from '@src/machines/modelingMachine'
import {
@ -621,7 +622,22 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[
{
id: 'constraint-length',
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }),
disabled: (state) =>
!(
state.matches({ Sketch: 'SketchIdle' }) &&
state.can({
type: 'Constrain length',
data: {
selection: state.context.selectionRanges,
// dummy data is okay for checking if the constrain is possible
length: {
valueAst: createLiteral(1),
valueText: '1',
valueCalculated: '1',
},
},
})
),
onClick: () =>
commandBarActor.send({
type: 'Find and select command',

View File

@ -15,6 +15,7 @@ import { createProfileStartHandle } from '@src/clientSideScene/segments'
import type { MachineManager } from '@src/components/MachineManagerProvider'
import type { ModelingMachineContext } from '@src/components/ModelingMachineProvider'
import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes'
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
import {
applyConstraintEqualAngle,
equalAngleInfo,
@ -41,7 +42,6 @@ import {
applyConstraintHorzVertAlign,
horzVertDistanceInfo,
} from '@src/components/Toolbar/SetHorzVertDistance'
import { angleLengthInfo } from '@src/components/Toolbar/setAngleLength'
import { createLiteral, createLocalName } from '@src/lang/create'
import { updateModelingState } from '@src/lang/modelingWorkflows'
import {