Update selections after constraint is applied [equal length, parallel, snap to x or y] (#2543)
* migrate one constraint * typo * update snap to y, snap to x, horz align, vert align, equal length * add some e2e tests * add e2e test for snap to axis contsraits * remove works for now
This commit is contained in:
@ -2439,6 +2439,158 @@ test('Extrude from command bar selects extrude line after', async ({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Testing constraints', () => {
|
||||||
|
test.describe('Two segment - no modal constraints', () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
codeAfter: `|> angledLine([83, segLen('seg01', %)], %)`,
|
||||||
|
constraintName: 'Equal Length',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codeAfter: `|> angledLine([segAng('seg01', %), 78.33], %)`,
|
||||||
|
constraintName: 'Parallel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codeAfter: `|> lineTo([segEndX('seg01', %), 61.34], %)`,
|
||||||
|
constraintName: 'Vertically Align',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codeAfter: `|> lineTo([154.9, segEndY('seg01', %)], %)`,
|
||||||
|
constraintName: 'Horizontally Align',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
for (const { codeAfter, constraintName } of cases) {
|
||||||
|
test(`${constraintName}`, async ({ page }) => {
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const yo = 5
|
||||||
|
const part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([-7.54, -26.74], %)
|
||||||
|
|> line([74.36, 130.4], %)
|
||||||
|
|> line([78.92, -120.11], %)
|
||||||
|
|> line([9.16, 77.79], %)
|
||||||
|
const part002 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([299.05, 231.45], %)
|
||||||
|
|> xLine(-425.34, %, 'seg-what')
|
||||||
|
|> yLine(-264.06, %)
|
||||||
|
|> xLine(segLen('seg-what', %), %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await page.getByText('line([74.36, 130.4], %)').click()
|
||||||
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
|
|
||||||
|
const line1 = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
|
||||||
|
const line3 = await u.getBoundingBox(`[data-overlay-index="${2}"]`)
|
||||||
|
|
||||||
|
// select two segments by holding down shift
|
||||||
|
await page.mouse.click(line1.x - 20, line1.y + 20)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.mouse.click(line3.x - 3, line3.y + 20)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
const constraintMenuButton = page.getByRole('button', {
|
||||||
|
name: 'Constrain',
|
||||||
|
})
|
||||||
|
const constraintButton = page.getByRole('button', {
|
||||||
|
name: constraintName,
|
||||||
|
})
|
||||||
|
|
||||||
|
// apply the constraint
|
||||||
|
await constraintMenuButton.click()
|
||||||
|
await constraintButton.click()
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content')).toContainText(codeAfter)
|
||||||
|
// expect the string 'seg01' to appear twice in '.cm-content' the tag segment and referencing the tag
|
||||||
|
const content = await page.locator('.cm-content').innerText()
|
||||||
|
await expect(content.match(/seg01/g)).toHaveLength(2)
|
||||||
|
// check there are still 2 cursors (they should stay on the same lines as before constraint was applied)
|
||||||
|
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||||
|
// check actives lines
|
||||||
|
const activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||||
|
await expect(activeLinesContent).toHaveLength(2)
|
||||||
|
|
||||||
|
// check both cursors are where they should be after constraint is applied
|
||||||
|
await expect(activeLinesContent[0]).toHaveText(
|
||||||
|
"|> line([74.36, 130.4], %, 'seg01')"
|
||||||
|
)
|
||||||
|
await expect(activeLinesContent[1]).toHaveText(codeAfter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
test.describe('Axis & segment - no modal constraints', () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
codeAfter: `|> lineTo([154.9, ZERO], %)`,
|
||||||
|
axisClick: { x: 950, y: 250 },
|
||||||
|
constraintName: 'Snap To X',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codeAfter: `|> lineTo([ZERO, 61.34], %)`,
|
||||||
|
axisClick: { x: 600, y: 150 },
|
||||||
|
constraintName: 'Snap To Y',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
for (const { codeAfter, constraintName, axisClick } of cases) {
|
||||||
|
test(`${constraintName}`, async ({ page }) => {
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const yo = 5
|
||||||
|
const part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([-7.54, -26.74], %)
|
||||||
|
|> line([74.36, 130.4], %)
|
||||||
|
|> line([78.92, -120.11], %)
|
||||||
|
|> line([9.16, 77.79], %)
|
||||||
|
const part002 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([299.05, 231.45], %)
|
||||||
|
|> xLine(-425.34, %, 'seg-what')
|
||||||
|
|> yLine(-264.06, %)
|
||||||
|
|> xLine(segLen('seg-what', %), %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await page.getByText('line([74.36, 130.4], %)').click()
|
||||||
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
|
|
||||||
|
const line3 = await u.getBoundingBox(`[data-overlay-index="${2}"]`)
|
||||||
|
|
||||||
|
// select segment and axis by holding down shift
|
||||||
|
await page.mouse.click(line3.x - 3, line3.y + 20)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.mouse.click(axisClick.x, axisClick.y)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
const constraintMenuButton = page.getByRole('button', {
|
||||||
|
name: 'Constrain',
|
||||||
|
})
|
||||||
|
const constraintButton = page.getByRole('button', {
|
||||||
|
name: constraintName,
|
||||||
|
})
|
||||||
|
|
||||||
|
// apply the constraint
|
||||||
|
await constraintMenuButton.click()
|
||||||
|
await expect(constraintButton).toBeVisible()
|
||||||
|
await constraintButton.click()
|
||||||
|
|
||||||
|
// check the cursor is where is should be after constraint is applied
|
||||||
|
await expect(page.locator('.cm-content')).toContainText(codeAfter)
|
||||||
|
await expect(page.locator('.cm-activeLine')).toHaveText(codeAfter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test.describe('Testing segment overlays', () => {
|
test.describe('Testing segment overlays', () => {
|
||||||
test.describe('Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments', () => {
|
test.describe('Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments', () => {
|
||||||
/**
|
/**
|
||||||
|
@ -18,7 +18,7 @@ export default defineConfig({
|
|||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 3 : 0,
|
retries: process.env.CI ? 3 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 2 : 1,
|
workers: process.env.CI ? 1 : 1,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
@ -34,6 +34,7 @@ import {
|
|||||||
handleSelectionBatch,
|
handleSelectionBatch,
|
||||||
isSelectionLastLine,
|
isSelectionLastLine,
|
||||||
isSketchPipe,
|
isSketchPipe,
|
||||||
|
updateSelections,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
@ -53,6 +54,7 @@ import {
|
|||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
|
Value,
|
||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
coreDump,
|
coreDump,
|
||||||
parse,
|
parse,
|
||||||
@ -73,10 +75,7 @@ import { useSearchParams } from 'react-router-dom'
|
|||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
||||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||||
import {
|
import { applyConstraintEqualAngle } from './Toolbar/EqualAngle'
|
||||||
EngineConnectionState,
|
|
||||||
EngineConnectionStateType,
|
|
||||||
} from 'lang/std/engineConnection'
|
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -223,8 +222,7 @@ export const ModelingMachineProvider = ({
|
|||||||
: {}
|
: {}
|
||||||
),
|
),
|
||||||
'Set selection': assign(({ selectionRanges }, event) => {
|
'Set selection': assign(({ selectionRanges }, event) => {
|
||||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
const setSelections = event.data as SetSelections // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||||
const setSelections = event.data
|
|
||||||
if (!editorManager.editorView) return {}
|
if (!editorManager.editorView) return {}
|
||||||
const dispatchSelection = (selection?: EditorSelection) => {
|
const dispatchSelection = (selection?: EditorSelection) => {
|
||||||
if (!selection) return // TODO less of hack for the below please
|
if (!selection) return // TODO less of hack for the below please
|
||||||
@ -311,6 +309,12 @@ export const ModelingMachineProvider = ({
|
|||||||
selectionRanges: selections,
|
selectionRanges: selections,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (setSelections.selectionType === 'completeSelection') {
|
||||||
|
editorManager.selectRange(setSelections.selection)
|
||||||
|
return {
|
||||||
|
selectionRanges: setSelections.selection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}),
|
}),
|
||||||
|
@ -144,7 +144,7 @@ export async function applyConstraintAngleLength({
|
|||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('erorr', e)
|
console.log('error', e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
kclManager,
|
kclManager,
|
||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { CallExpression, SourceRange, parse, recast } from 'lang/wasm'
|
import { CallExpression, SourceRange, Value, parse, recast } from 'lang/wasm'
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { EditorSelection } from '@codemirror/state'
|
import { EditorSelection } from '@codemirror/state'
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
} from 'clientSideScene/sceneEntities'
|
} from 'clientSideScene/sceneEntities'
|
||||||
import { Mesh, Object3D, Object3DEventMap } from 'three'
|
import { Mesh, Object3D, Object3DEventMap } from 'three'
|
||||||
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
|
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
|
||||||
|
import { PathToNodeMap } from 'lang/std/sketchcombos'
|
||||||
|
|
||||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||||
@ -564,3 +565,22 @@ export function sendSelectEventToEngine(
|
|||||||
.then((res) => res.data.data)
|
.then((res) => res.data.data)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateSelections(
|
||||||
|
pathToNodeMap: PathToNodeMap,
|
||||||
|
prevSelectionRanges: Selections,
|
||||||
|
ast: Program
|
||||||
|
): Selections {
|
||||||
|
return {
|
||||||
|
...prevSelectionRanges,
|
||||||
|
codeBasedSelections: Object.entries(pathToNodeMap).map(
|
||||||
|
([index, pathToNode]): Selection => {
|
||||||
|
const node = getNodeFromPath<Value>(ast, pathToNode).node
|
||||||
|
return {
|
||||||
|
range: [node.start, node.end],
|
||||||
|
type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user