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:
@ -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()
|
||||
})
|
||||
}
|
||||
|
@ -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',
|
||||
|
57
src/components/Toolbar/angleLengthInfo.ts
Normal file
57
src/components/Toolbar/angleLengthInfo.ts
Normal 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 }
|
||||
}
|
@ -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,
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user