diff --git a/.env.production b/.env.production index cecfb2cc1..a3950379a 100644 --- a/.env.production +++ b/.env.production @@ -3,4 +3,4 @@ VITE_KC_API_BASE_URL=https://api.zoo.dev VITE_KC_SITE_BASE_URL=https://zoo.dev VITE_KC_SKIP_AUTH=false VITE_KC_CONNECTION_TIMEOUT_MS=15000 -VITE_KC_SENTRY_DSN=https://a814f2f66734989a90367f48feee28ca@o1042111.ingest.sentry.io/4505789425844224 +VITE_KC_SENTRY_DSN= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 869ca7356..adf5fe815 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -378,6 +378,7 @@ jobs: announce_release: needs: [publish-apps-release] runs-on: ubuntu-latest + if: github.event_name == 'release' steps: - name: Check out code uses: actions/checkout@v4 diff --git a/docs/kcl/KNOWN-ISSUES.md b/docs/kcl/KNOWN-ISSUES.md index 0d2aec5b0..c2e51c2de 100644 --- a/docs/kcl/KNOWN-ISSUES.md +++ b/docs/kcl/KNOWN-ISSUES.md @@ -6,11 +6,13 @@ once fixed in engine will just start working here with no language changes. - **Sketch on Face**: If your sketch is outside the edges of the face (on which you are sketching) you will get multiple models returned instead of one single model for that sketch and its underlying 3D object. + If you see a red line around your model, it means this is happening. - **Patterns**: If you try and pass a pattern to `hole` currently only the first item in the pattern is being subtracted. This is an engine bug that is being worked on. - **Import**: Right now you can import a file, even if that file has brep data - you cannot edit it. You also cannot move or transform the imported objects at - all. In the future, after v1, the engine will account for this. + you cannot edit it, after v1, the engine will account for this. You also cannot + currently move or transform the imported objects at all, once we have assemblies + this will work. diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index 29f4c78e1..a3c3ca972 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -637,12 +637,15 @@ test('Can extrude from the command bar', async ({ page, context }) => { await context.addInitScript(async (token) => { localStorage.setItem( 'persistCode', - `const part001 = startSketchOn('-XZ') - |> startProfileAt([-6.95, 4.98], %) - |> line([25.1, 0.41], %) - |> line([0.73, -14.93], %) - |> line([-23.44, 0.52], %) - |> close(%)` + ` + const distance = sqrt(20) + const part001 = startSketchOn('-XZ') + |> startProfileAt([-6.95, 4.98], %) + |> line([25.1, 0.41], %) + |> line([0.73, -14.93], %) + |> line([-23.44, 0.52], %) + |> close(%) + ` ) }) @@ -667,24 +670,42 @@ test('Can extrude from the command bar', async ({ page, context }) => { // Click to select face and set distance await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click() await page.getByRole('button', { name: 'Continue' }).click() + + // Assert that we're on the distance step await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled() - await page.keyboard.press('Enter') + + // Assert that the an alternative variable name is chosen, + // since the default variable name is already in use (distance) + await page.getByRole('button', { name: 'Create new variable' }).click() + await expect(page.getByPlaceholder('Variable name')).toHaveValue( + 'distance001' + ) + await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled() + await page.getByRole('button', { name: 'Continue' }).click() // Review step and argument hotkeys - await page.keyboard.press('2') - await expect(page.getByRole('button', { name: '5' })).toBeDisabled() + await expect( + page.getByRole('button', { name: 'Submit command' }) + ).toBeEnabled() + await page.keyboard.press('Backspace') + await expect( + page.getByRole('button', { name: 'Distance 12', exact: false }) + ).toBeDisabled() await page.keyboard.press('Enter') // Check that the code was updated await page.keyboard.press('Enter') + // Unfortunately this indentation seems to matter for the test await expect(page.locator('.cm-content')).toHaveText( - `const part001 = startSketchOn('-XZ') - |> startProfileAt([-6.95, 4.98], %) - |> line([25.1, 0.41], %) - |> line([0.73, -14.93], %) - |> line([-23.44, 0.52], %) - |> close(%) - |> extrude(5, %)` + `const distance = sqrt(20) +const distance001 = 5 + 7 +const part001 = startSketchOn('-XZ') + |> startProfileAt([-6.95, 4.98], %) + |> line([25.1, 0.41], %) + |> line([0.73, -14.93], %) + |> line([-23.44, 0.52], %) + |> close(%) + |> extrude(distance001, %)`.replace(/(\r\n|\n|\r)/gm, '') // remove newlines ) }) diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png index b36bdc853..880b04943 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png index ac86c5469..5d941054e 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png differ diff --git a/src/Router.tsx b/src/Router.tsx index 4846bdcbe..6a4b0f905 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -34,7 +34,9 @@ import { settingsMachine, } from './machines/settingsMachine' import { ContextFrom } from 'xstate' -import CommandBarProvider from 'components/CommandBar/CommandBar' +import CommandBarProvider, { + CommandBar, +} from 'components/CommandBar/CommandBar' import { TEST, VITE_KC_SENTRY_DSN } from './env' import * as Sentry from '@sentry/react' import ModelingMachineProvider from 'components/ModelingMachineProvider' @@ -117,6 +119,7 @@ const router = createBrowserRouter( + @@ -218,6 +221,7 @@ const router = createBrowserRouter( + ), loader: async (): Promise => { diff --git a/src/components/AvailableVarsHelpers.tsx b/src/components/AvailableVarsHelpers.tsx index ff6e8aaa8..a9c53f112 100644 --- a/src/components/AvailableVarsHelpers.tsx +++ b/src/components/AvailableVarsHelpers.tsx @@ -87,7 +87,7 @@ export function useCalc({ inputRef: React.RefObject valueNode: Value | null calcResult: string - prevVariables: PrevVariable[] + prevVariables: PrevVariable[] newVariableName: string isNewVariableNameUnique: boolean newVariableInsertIndex: number diff --git a/src/components/CommandBar/CommandBar.tsx b/src/components/CommandBar/CommandBar.tsx index 2d0d5ff40..bded5abf6 100644 --- a/src/components/CommandBar/CommandBar.tsx +++ b/src/components/CommandBar/CommandBar.tsx @@ -57,12 +57,11 @@ export const CommandBarProvider = ({ }} > {children} - ) } -const CommandBar = () => { +export const CommandBar = () => { const { commandBarState, commandBarSend } = useCommandsContext() const { context: { selectedCommand, currentArgument, commands }, @@ -84,17 +83,25 @@ const CommandBar = () => { if (commandBarState.matches('Review')) { const entries = Object.entries(selectedCommand?.args || {}) - commandBarSend({ - type: commandBarState.matches('Review') - ? 'Edit argument' - : 'Change current argument', - data: { - arg: { - name: entries[entries.length - 1][0], - ...entries[entries.length - 1][1], + const currentArgName = entries[entries.length - 1][0] + const currentArg = { + name: currentArgName, + ...entries[entries.length - 1][1], + } + + if (commandBarState.matches('Review')) { + commandBarSend({ + type: 'Edit argument', + data: { + arg: currentArg, }, - }, - }) + }) + } else { + commandBarSend({ + type: 'Remove argument', + data: { [currentArgName]: currentArg }, + }) + } } else { commandBarSend({ type: 'Deselect command' }) } @@ -117,6 +124,11 @@ const CommandBar = () => { } } + useEffect( + () => console.log(commandBarState.context.argumentsToSubmit), + [commandBarState.context.argumentsToSubmit] + ) + return ( void }) { const { commandBarState, commandBarSend } = useCommandsContext() @@ -17,10 +18,7 @@ function CommandBarArgument({ stepBack }: { stepBack: () => void }) { commandBarSend({ type: 'Submit argument', data: { - [currentArgument.name]: - currentArgument.inputType === 'number' - ? parseFloat((data as string) || '0') - : data, + [currentArgument.name]: data, }, }) } @@ -68,6 +66,10 @@ function ArgumentInput({ onSubmit={onSubmit} /> ) + case 'kcl': + return ( + + ) default: return ( & { - inputType: 'number' | 'string' + inputType: 'string' name: string } stepBack: () => void @@ -18,7 +18,6 @@ function CommandBarBasicInput({ const { commandBarSend, commandBarState } = useCommandsContext() useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) const inputRef = useRef(null) - const inputType = arg.inputType === 'number' ? 'number' : 'text' useEffect(() => { if (inputRef.current) { @@ -40,9 +39,9 @@ function CommandBarBasicInput({ ) { const { commandBarState, commandBarSend } = useCommandsContext() @@ -45,6 +48,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) { parseInt(b.keys[0], 10) - 1 ] const arg = selectedCommand?.args[argName] + if (!argName || !arg) return commandBarSend({ type: 'Change current argument', data: { arg: { ...arg, name: argName } }, @@ -59,7 +63,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) { selectedCommand && argumentsToSubmit && ( <> -
+

) { : 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80' }`} > + {argName} {argumentsToSubmit[argName] ? ( arg.inputType === 'selection' ? ( getSelectionTypeDisplayText( argumentsToSubmit[argName] as Selections ) + ) : arg.inputType === 'kcl' ? ( + roundOff( + Number( + (argumentsToSubmit[argName] as KclCommandValue) + .valueCalculated + ), + 4 + ) ) : typeof argumentsToSubmit[argName] === 'object' ? ( JSON.stringify(argumentsToSubmit[argName]) ) : ( {argumentsToSubmit[argName] as ReactNode} ) - ) : ( - {argName} - )} + ) : null} {showShortcuts && ( Hotkey: {i + 1} )} + {arg.inputType === 'kcl' && + !!argumentsToSubmit[argName] && + 'variableName' in + (argumentsToSubmit[argName] as KclCommandValue) && ( + <> + + + New variable:{' '} + { + ( + argumentsToSubmit[ + argName + ] as KclExpressionWithVariable + ).variableName + } + + + )} ) )} diff --git a/src/components/CommandBar/CommandBarKclInput.module.css b/src/components/CommandBar/CommandBarKclInput.module.css new file mode 100644 index 000000000..ff7902848 --- /dev/null +++ b/src/components/CommandBar/CommandBarKclInput.module.css @@ -0,0 +1,17 @@ +.editor { + @apply text-base flex-1; +} + +.editor :global(.cm-editor) { + @apply bg-transparent; +} + +.editor :global(.cm-line)::selection { + @apply px-1; + @apply text-chalkboard-100; + @apply bg-energy-10/50; +} +:global(.dark) .editor :global(.cm-line)::selection { + @apply text-energy-10; + @apply bg-energy-10/20; +} diff --git a/src/components/CommandBar/CommandBarKclInput.tsx b/src/components/CommandBar/CommandBarKclInput.tsx new file mode 100644 index 000000000..067530a87 --- /dev/null +++ b/src/components/CommandBar/CommandBarKclInput.tsx @@ -0,0 +1,221 @@ +import { Completion } from '@codemirror/autocomplete' +import { EditorState, EditorView, useCodeMirror } from '@uiw/react-codemirror' +import { CustomIcon } from 'components/CustomIcon' +import { useCommandsContext } from 'hooks/useCommandsContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' +import { CommandArgument, KclCommandValue } from 'lib/commandTypes' +import { getSystemTheme } from 'lib/theme' +import { useCalculateKclExpression } from 'lib/useCalculateKclExpression' +import { roundOff } from 'lib/utils' +import { varMentions } from 'lib/varCompletionExtension' +import { useEffect, useRef, useState } from 'react' +import { useHotkeys } from 'react-hotkeys-hook' +import styles from './CommandBarKclInput.module.css' +import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst' + +function CommandBarKclInput({ + arg, + stepBack, + onSubmit, +}: { + arg: CommandArgument & { + inputType: 'kcl' + name: string + } + stepBack: () => void + onSubmit: (event: unknown) => void +}) { + const { commandBarSend, commandBarState } = useCommandsContext() + const previouslySetValue = commandBarState.context.argumentsToSubmit[ + arg.name + ] as KclCommandValue | undefined + const { settings } = useGlobalStateContext() + const defaultValue = (arg.defaultValue as string) || '' + const [value, setValue] = useState( + previouslySetValue?.valueText || defaultValue || '' + ) + const [createNewVariable, setCreateNewVariable] = useState( + previouslySetValue && 'variableName' in previouslySetValue + ) + const [canSubmit, setCanSubmit] = useState(true) + useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) + const editorRef = useRef(null) + + const { + prevVariables, + calcResult, + newVariableInsertIndex, + valueNode, + newVariableName, + setNewVariableName, + isNewVariableNameUnique, + } = useCalculateKclExpression({ + value, + initialVariableName: + previouslySetValue && 'variableName' in previouslySetValue + ? previouslySetValue.variableName + : arg.name, + }) + const varMentionData: Completion[] = prevVariables.map((v) => ({ + label: v.key, + detail: String(roundOff(v.value as number)), + })) + + const { setContainer } = useCodeMirror({ + container: editorRef.current, + value, + indentWithTab: false, + basicSetup: false, + autoFocus: true, + selection: { + anchor: 0, + head: + previouslySetValue && 'valueText' in previouslySetValue + ? previouslySetValue.valueText.length + : defaultValue.length, + }, + accessKey: 'command-bar', + theme: + settings.context.theme === 'system' + ? getSystemTheme() + : settings.context.theme, + extensions: [ + EditorView.domEventHandlers({ + keydown: (event) => { + if (event.key === 'Backspace' && value === '') { + event.preventDefault() + stepBack() + } + }, + }), + varMentions(varMentionData), + EditorState.transactionFilter.of((tr) => { + if (tr.newDoc.lines > 1) { + handleSubmit() + return [] + } + return tr + }), + ], + onChange: (newValue) => setValue(newValue), + }) + + useEffect(() => { + if (editorRef.current) { + setContainer(editorRef.current) + } + }, [arg, editorRef]) + + useEffect(() => { + setCanSubmit( + calcResult !== 'NAN' && (!createNewVariable || isNewVariableNameUnique) + ) + }, [calcResult, createNewVariable, isNewVariableNameUnique]) + + function handleSubmit(e?: React.FormEvent) { + e?.preventDefault() + if (!canSubmit || valueNode === null) return + + onSubmit( + createNewVariable + ? ({ + valueAst: valueNode, + valueText: value, + valueCalculated: calcResult, + variableName: newVariableName, + insertIndex: newVariableInsertIndex, + variableIdentifierAst: createIdentifier(newVariableName), + variableDeclarationAst: createVariableDeclaration( + newVariableName, + valueNode + ), + } satisfies KclCommandValue) + : ({ + valueAst: valueNode, + valueText: value, + valueCalculated: calcResult, + } satisfies KclCommandValue) + ) + } + + return ( +

+