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('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 */
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
/* 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: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
|
@ -34,6 +34,7 @@ import {
|
||||
handleSelectionBatch,
|
||||
isSelectionLastLine,
|
||||
isSketchPipe,
|
||||
updateSelections,
|
||||
} from 'lib/selections'
|
||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||
@ -53,6 +54,7 @@ import {
|
||||
} from 'lang/modifyAst'
|
||||
import {
|
||||
Program,
|
||||
Value,
|
||||
VariableDeclaration,
|
||||
coreDump,
|
||||
parse,
|
||||
@ -73,10 +75,7 @@ import { useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import {
|
||||
EngineConnectionState,
|
||||
EngineConnectionStateType,
|
||||
} from 'lang/std/engineConnection'
|
||||
import { applyConstraintEqualAngle } from './Toolbar/EqualAngle'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -223,8 +222,7 @@ export const ModelingMachineProvider = ({
|
||||
: {}
|
||||
),
|
||||
'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
|
||||
const setSelections = event.data as SetSelections // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||
if (!editorManager.editorView) return {}
|
||||
const dispatchSelection = (selection?: EditorSelection) => {
|
||||
if (!selection) return // TODO less of hack for the below please
|
||||
@ -311,6 +309,12 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges: selections,
|
||||
}
|
||||
}
|
||||
if (setSelections.selectionType === 'completeSelection') {
|
||||
editorManager.selectRange(setSelections.selection)
|
||||
return {
|
||||
selectionRanges: setSelections.selection,
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}),
|
||||
|
@ -144,7 +144,7 @@ export async function applyConstraintAngleLength({
|
||||
pathToNodeMap,
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('erorr', e)
|
||||
console.log('error', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
kclManager,
|
||||
sceneEntitiesManager,
|
||||
} 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 { uuidv4 } from 'lib/utils'
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
@ -27,6 +27,7 @@ import {
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
import { Mesh, Object3D, Object3DEventMap } from 'three'
|
||||
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 Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||
@ -564,3 +565,22 @@ export function sendSelectEventToEngine(
|
||||
.then((res) => res.data.data)
|
||||
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