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(
|
test(
|
||||||
'Able to double click label to set constraint',
|
'Able to double click label to set constraint',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ page, context, homePage, scene, editor, toolbar }) => {
|
async ({ page, context, homePage, scene, editor, toolbar, cmdBar }) => {
|
||||||
await context.folderSetupFn(async (dir) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
const bracketDir = path.join(dir, 'test-sample')
|
const bracketDir = path.join(dir, 'test-sample')
|
||||||
await fsp.mkdir(bracketDir, { recursive: true })
|
await fsp.mkdir(bracketDir, { recursive: true })
|
||||||
@ -1132,6 +1132,14 @@ test.describe('Electron constraint tests', () => {
|
|||||||
await scene.waitForExecutionDone()
|
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 () => {
|
await test.step('Double click to constrain', async () => {
|
||||||
// Enter sketch edit mode via feature tree
|
// Enter sketch edit mode via feature tree
|
||||||
await toolbar.openPane('feature-tree')
|
await toolbar.openPane('feature-tree')
|
||||||
@ -1139,21 +1147,19 @@ test.describe('Electron constraint tests', () => {
|
|||||||
await op.dblclick()
|
await op.dblclick()
|
||||||
await toolbar.closePane('feature-tree')
|
await toolbar.closePane('feature-tree')
|
||||||
|
|
||||||
const child = page
|
await clickOnFirstSegmentLabel()
|
||||||
.locator('.segment-length-label-text')
|
await cmdBar.progressCmdBar()
|
||||||
.first()
|
await editor.expectEditor.toContain('length001 = 15.3')
|
||||||
.locator('xpath=..')
|
await editor.expectEditor.toContain('|> angledLine([9, length001], %)')
|
||||||
await child.dblclick()
|
})
|
||||||
const cmdBarSubmitButton = page.getByRole('button', {
|
|
||||||
name: 'arrow right Continue',
|
await test.step('Double click again and expect failure', async () => {
|
||||||
})
|
await clickOnFirstSegmentLabel()
|
||||||
await cmdBarSubmitButton.click()
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
await expect(
|
||||||
'length001 = 15.3'
|
page.getByText('Unable to constrain the length of this segment')
|
||||||
)
|
).toBeVisible()
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'|> angledLine([9, length001], %)'
|
|
||||||
)
|
|
||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,7 @@ import {
|
|||||||
SEGMENT_LENGTH_LABEL_OFFSET_PX,
|
SEGMENT_LENGTH_LABEL_OFFSET_PX,
|
||||||
SEGMENT_LENGTH_LABEL_TEXT,
|
SEGMENT_LENGTH_LABEL_TEXT,
|
||||||
} from '@src/clientSideScene/sceneInfra'
|
} from '@src/clientSideScene/sceneInfra'
|
||||||
|
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
|
||||||
import type { Coords2d } from '@src/lang/std/sketch'
|
import type { Coords2d } from '@src/lang/std/sketch'
|
||||||
import type { SegmentInputs } from '@src/lang/std/stdTypes'
|
import type { SegmentInputs } from '@src/lang/std/stdTypes'
|
||||||
import type { PathToNode } from '@src/lang/wasm'
|
import type { PathToNode } from '@src/lang/wasm'
|
||||||
@ -88,6 +89,7 @@ import type {
|
|||||||
SegmentOverlayPayload,
|
SegmentOverlayPayload,
|
||||||
SegmentOverlays,
|
SegmentOverlays,
|
||||||
} from '@src/machines/modelingMachine'
|
} from '@src/machines/modelingMachine'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
const ANGLE_INDICATOR_RADIUS = 30 // in px
|
const ANGLE_INDICATOR_RADIUS = 30 // in px
|
||||||
interface CreateSegmentArgs {
|
interface CreateSegmentArgs {
|
||||||
@ -1710,6 +1712,7 @@ function createLengthIndicator({
|
|||||||
console.error('Unable to dimension segment when clicking the label.')
|
console.error('Unable to dimension segment when clicking the label.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sceneInfra.modelingSend({
|
sceneInfra.modelingSend({
|
||||||
type: 'Set selection',
|
type: 'Set selection',
|
||||||
data: {
|
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
|
// Command Bar
|
||||||
commandBarActor.send({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
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,
|
SetAngleLengthModal,
|
||||||
createSetAngleLengthModal,
|
createSetAngleLengthModal,
|
||||||
} from '@src/components/SetAngleLengthModal'
|
} from '@src/components/SetAngleLengthModal'
|
||||||
|
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
|
||||||
import {
|
import {
|
||||||
createBinaryExpressionWithUnary,
|
createBinaryExpressionWithUnary,
|
||||||
createLocalName,
|
createLocalName,
|
||||||
createName,
|
createName,
|
||||||
createVariableDeclaration,
|
createVariableDeclaration,
|
||||||
} from '@src/lang/create'
|
} 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 type { PathToNodeMap } from '@src/lang/std/sketchcombos'
|
||||||
import {
|
import {
|
||||||
getTransformInfos,
|
|
||||||
isExprBinaryPart,
|
isExprBinaryPart,
|
||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from '@src/lang/std/sketchcombos'
|
} from '@src/lang/std/sketchcombos'
|
||||||
import type { TransformInfo } from '@src/lang/std/stdTypes'
|
|
||||||
import type { Expr, Program } from '@src/lang/wasm'
|
import type { Expr, Program } from '@src/lang/wasm'
|
||||||
import type { KclCommandValue } from '@src/lib/commandTypes'
|
import type { KclCommandValue } from '@src/lib/commandTypes'
|
||||||
import type { Selections } from '@src/lib/selections'
|
import type { Selections } from '@src/lib/selections'
|
||||||
@ -27,48 +24,6 @@ import { normaliseAngle } from '@src/lib/utils'
|
|||||||
|
|
||||||
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
|
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({
|
export async function applyConstraintLength({
|
||||||
length,
|
length,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Models } from '@kittycad/lib'
|
import type { Models } from '@kittycad/lib'
|
||||||
import { DEV } from '@src/env'
|
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 { getNodeFromPath } from '@src/lang/queryAst'
|
||||||
import { getVariableDeclaration } from '@src/lang/queryAst/getVariableDeclaration'
|
import { getVariableDeclaration } from '@src/lang/queryAst/getVariableDeclaration'
|
||||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||||
|
@ -2,6 +2,7 @@ import { DEV } from '@src/env'
|
|||||||
import type { EventFrom, StateFrom } from 'xstate'
|
import type { EventFrom, StateFrom } from 'xstate'
|
||||||
|
|
||||||
import type { CustomIconName } from '@src/components/CustomIcon'
|
import type { CustomIconName } from '@src/components/CustomIcon'
|
||||||
|
import { createLiteral } from '@src/lang/create'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import type { modelingMachine } from '@src/machines/modelingMachine'
|
import type { modelingMachine } from '@src/machines/modelingMachine'
|
||||||
import {
|
import {
|
||||||
@ -621,7 +622,22 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: 'constraint-length',
|
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: () =>
|
onClick: () =>
|
||||||
commandBarActor.send({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
|
@ -15,6 +15,7 @@ import { createProfileStartHandle } from '@src/clientSideScene/segments'
|
|||||||
import type { MachineManager } from '@src/components/MachineManagerProvider'
|
import type { MachineManager } from '@src/components/MachineManagerProvider'
|
||||||
import type { ModelingMachineContext } from '@src/components/ModelingMachineProvider'
|
import type { ModelingMachineContext } from '@src/components/ModelingMachineProvider'
|
||||||
import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes'
|
import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes'
|
||||||
|
import { angleLengthInfo } from '@src/components/Toolbar/angleLengthInfo'
|
||||||
import {
|
import {
|
||||||
applyConstraintEqualAngle,
|
applyConstraintEqualAngle,
|
||||||
equalAngleInfo,
|
equalAngleInfo,
|
||||||
@ -41,7 +42,6 @@ import {
|
|||||||
applyConstraintHorzVertAlign,
|
applyConstraintHorzVertAlign,
|
||||||
horzVertDistanceInfo,
|
horzVertDistanceInfo,
|
||||||
} from '@src/components/Toolbar/SetHorzVertDistance'
|
} from '@src/components/Toolbar/SetHorzVertDistance'
|
||||||
import { angleLengthInfo } from '@src/components/Toolbar/setAngleLength'
|
|
||||||
import { createLiteral, createLocalName } from '@src/lang/create'
|
import { createLiteral, createLocalName } from '@src/lang/create'
|
||||||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||||||
import {
|
import {
|
||||||
|
Reference in New Issue
Block a user