diff --git a/e2e/playwright/testing-settings.spec.ts b/e2e/playwright/testing-settings.spec.ts index 83dd0306f..61721aef7 100644 --- a/e2e/playwright/testing-settings.spec.ts +++ b/e2e/playwright/testing-settings.spec.ts @@ -896,4 +896,53 @@ test.describe('Testing settings', () => { }) } ) + + test(`Change inline units setting`, async ({ + page, + homePage, + context, + editor, + }) => { + const initialInlineUnits = 'yd' + const editedInlineUnits = { short: 'mm', long: 'Millimeters' } + const inlineSettingsString = (s: string) => + `@settings(defaultLengthUnit = ${s})` + const unitsIndicator = page.getByRole('button', { + name: 'Current units are:', + }) + const unitsChangeButton = (name: string) => + page.getByRole('button', { name, exact: true }) + + await context.folderSetupFn(async (dir) => { + const bracketDir = join(dir, 'project-000') + await fsp.mkdir(bracketDir, { recursive: true }) + await fsp.copyFile( + executorInputPath('cube.kcl'), + join(bracketDir, 'main.kcl') + ) + }) + + await test.step(`Initial units from settings`, async () => { + await homePage.openProject('project-000') + await expect(unitsIndicator).toHaveText('Current units are: in') + }) + + await test.step(`Manually write inline settings`, async () => { + await editor.openPane() + await editor.replaceCode( + `fn cube`, + `${inlineSettingsString(initialInlineUnits)} +fn cube` + ) + await expect(unitsIndicator).toContainText(initialInlineUnits) + }) + + await test.step(`Change units setting via lower-right control`, async () => { + await unitsIndicator.click() + await unitsChangeButton(editedInlineUnits.long).click() + await expect( + page.getByText(`Updated per-file units to ${editedInlineUnits.short}`) + ).toBeVisible() + }) + }) }) diff --git a/src/components/UnitsMenu.tsx b/src/components/UnitsMenu.tsx index 4b9c7f41d..01d28b8bb 100644 --- a/src/components/UnitsMenu.tsx +++ b/src/components/UnitsMenu.tsx @@ -1,9 +1,31 @@ import { Popover } from '@headlessui/react' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { changeKclSettings, unitLengthToUnitLen } from 'lang/wasm' import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes' +import { codeManager, kclManager } from 'lib/singletons' +import { err, reportRejection } from 'lib/trap' +import { useEffect, useState } from 'react' +import toast from 'react-hot-toast' export function UnitsMenu() { const { settings } = useSettingsAuthContext() + const [hasPerFileLengthUnit, setHasPerFileLengthUnit] = useState( + Boolean(kclManager.fileSettings.defaultLengthUnit) + ) + const [lengthSetting, setLengthSetting] = useState( + kclManager.fileSettings.defaultLengthUnit || + settings.context.modeling.defaultUnit.current + ) + useEffect(() => { + setHasPerFileLengthUnit(Boolean(kclManager.fileSettings.defaultLengthUnit)) + setLengthSetting( + kclManager.fileSettings.defaultLengthUnit || + settings.context.modeling.defaultUnit.current + ) + }, [ + kclManager.fileSettings.defaultLengthUnit, + settings.context.modeling.defaultUnit.current, + ]) return ( {({ close }) => ( @@ -18,7 +40,7 @@ export function UnitsMenu() {
Current units are:  - {settings.context.modeling.defaultUnit.current} + {lengthSetting} { - settings.send({ - type: 'set.modeling.defaultUnit', - data: { - level: 'project', - value: unit, - }, - }) + if (hasPerFileLengthUnit) { + const newCode = changeKclSettings(codeManager.code, { + defaultLengthUnits: unitLengthToUnitLen(unit), + defaultAngleUnits: { type: 'Degrees' }, + }) + if (err(newCode)) { + toast.error( + `Failed to set per-file units: ${newCode.message}` + ) + } else { + codeManager.updateCodeStateEditor(newCode) + Promise.all([ + codeManager.writeToFile(), + kclManager.executeCode(), + ]) + .then(() => { + toast.success(`Updated per-file units to ${unit}`) + }) + .catch(reportRejection) + } + } else { + settings.send({ + type: 'set.modeling.defaultUnit', + data: { + level: 'project', + value: unit, + }, + }) + } close() }} > {baseUnitLabels[unit]} - {unit === settings.context.modeling.defaultUnit.current && ( + {unit === lengthSetting && ( current )} diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index 0a06ef1b8..d834eca5c 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -25,7 +25,7 @@ import { SourceRange, topLevelRange, } from 'lang/wasm' -import { getNodeFromPath } from './queryAst' +import { getNodeFromPath, getSettingsAnnotation } from './queryAst' import { codeManager, editorManager, sceneInfra } from 'lib/singletons' import { Diagnostic } from '@codemirror/lint' import { markOnce } from 'lib/performance' @@ -35,6 +35,7 @@ import { ModelingCmdReq_type, } from '@kittycad/lib/dist/types/src/models' import { Operation } from 'wasm-lib/kcl/bindings/Operation' +import { KclSettingsAnnotation } from 'lib/settings/settingsTypes' interface ExecuteArgs { ast?: Node @@ -70,6 +71,7 @@ export class KclManager { private _wasmInitFailed = true private _hasErrors = false private _switchedFiles = false + private _fileSettings: KclSettingsAnnotation = {} engineCommandManager: EngineCommandManager @@ -368,6 +370,13 @@ export class KclManager { await this.disableSketchMode() } + let fileSettings = getSettingsAnnotation(ast) + if (err(fileSettings)) { + console.error(fileSettings) + fileSettings = {} + } + this.fileSettings = fileSettings + this.logs = logs this.errors = errors // Do not add the errors since the program was interrupted and the error is not a real KCL error @@ -699,6 +708,14 @@ export class KclManager { _isAstEmpty(ast: Node) { return ast.start === 0 && ast.end === 0 && ast.body.length === 0 } + + get fileSettings() { + return this._fileSettings + } + + set fileSettings(settings: KclSettingsAnnotation) { + this._fileSettings = settings + } } const defaultSelectionFilter: EntityType_type[] = [ diff --git a/src/lang/queryAst.ts b/src/lang/queryAst.ts index e22363867..64b918b6f 100644 --- a/src/lang/queryAst.ts +++ b/src/lang/queryAst.ts @@ -23,6 +23,9 @@ import { VariableDeclaration, VariableDeclarator, recast, + kclSettings, + unitLenToUnitLength, + unitAngToUnitAngle, } from './wasm' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { createIdentifier, splitPathAtLastIndex } from './modifyAst' @@ -38,6 +41,7 @@ import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement' import { Node } from 'wasm-lib/kcl/bindings/Node' import { findKwArg } from './util' import { codeRefFromRange } from './std/artifactGraph' +import { KclSettingsAnnotation } from 'lib/settings/settingsTypes' export const LABELED_ARG_FIELD = 'LabeledArg -> Arg' export const ARG_INDEX_FIELD = 'arg index' @@ -866,3 +870,24 @@ export function getObjExprProperty( if (index === -1) return null return { expr: node.properties[index].value, index } } + +/** + * Given KCL, returns the settings annotation object if it exists. + */ +export function getSettingsAnnotation( + kcl: string | Node +): KclSettingsAnnotation | Error { + const metaSettings = kclSettings(kcl) + if (err(metaSettings)) return metaSettings + + const settings: KclSettingsAnnotation = {} + // No settings in the KCL. + if (!metaSettings) return settings + + settings.defaultLengthUnit = unitLenToUnitLength( + metaSettings.defaultLengthUnits + ) + settings.defaultAngleUnit = unitAngToUnitAngle(metaSettings.defaultAngleUnits) + + return settings +} diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index 5586121cb..b99f09e12 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -18,6 +18,7 @@ import { default_project_settings, base64_decode, clear_scene_and_bust_cache, + kcl_settings, change_kcl_settings, reloadModule, } from 'lib/wasm_lib_wrapper' @@ -58,6 +59,9 @@ import { Artifact } from './std/artifactGraph' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix' import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings' +import { UnitAngle, UnitLength } from 'wasm-lib/kcl/bindings/ModelingCmd' +import { UnitLen } from 'wasm-lib/kcl/bindings/UnitLen' +import { UnitAngle as UnitAng } from 'wasm-lib/kcl/bindings/UnitAngle' export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact' export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact' @@ -857,8 +861,35 @@ export function base64Decode(base64: string): ArrayBuffer | Error { } } -/// Change the meta settings for the kcl file. -/// Returns the new kcl string with the updated settings. +/** + * Get the meta settings for the KCL. If no settings were set in the file, + * returns null. + */ +export function kclSettings( + kcl: string | Node +): MetaSettings | null | Error { + let program: Node + if (typeof kcl === 'string') { + const parseResult = parse(kcl) + if (err(parseResult)) return parseResult + if (!resultIsOk(parseResult)) { + return new Error(`parse result had errors`, { cause: parseResult }) + } + program = parseResult.program + } else { + program = kcl + } + try { + return kcl_settings(JSON.stringify(program)) + } catch (e) { + return new Error('Caught error getting kcl settings', { cause: e }) + } +} + +/** + * Change the meta settings for the kcl file. + * @returns the new kcl string with the updated settings. + */ export function changeKclSettings( kcl: string, settings: MetaSettings @@ -866,7 +897,59 @@ export function changeKclSettings( try { return change_kcl_settings(kcl, JSON.stringify(settings)) } catch (e) { - console.error('Caught error changing kcl settings: ' + e) - return new Error('Caught error changing kcl settings: ' + e) + console.error('Caught error changing kcl settings', e) + return new Error('Caught error changing kcl settings', { cause: e }) + } +} + +/** + * Convert a `UnitLength_type` to a `UnitLen` + */ +export function unitLengthToUnitLen(input: UnitLength): UnitLen { + switch (input) { + case 'm': + return { type: 'M' } + case 'cm': + return { type: 'Cm' } + case 'yd': + return { type: 'Yards' } + case 'ft': + return { type: 'Feet' } + case 'in': + return { type: 'Inches' } + default: + return { type: 'Mm' } + } +} + +/** + * Convert `UnitLen` to `UnitLength_type`. + */ +export function unitLenToUnitLength(input: UnitLen): UnitLength { + switch (input.type) { + case 'M': + return 'm' + case 'Cm': + return 'cm' + case 'Yards': + return 'yd' + case 'Feet': + return 'ft' + case 'Inches': + return 'in' + default: + return 'mm' + } +} + +/** + * Convert `UnitAngle` to `UnitAngle_type`. + */ +export function unitAngToUnitAngle(input: UnitAng): UnitAngle { + switch (input.type) { + case 'Radians': + return 'radians' + default: + return 'degrees' } } diff --git a/src/lib/settings/settingsTypes.ts b/src/lib/settings/settingsTypes.ts index a9de440eb..755ef0cde 100644 --- a/src/lib/settings/settingsTypes.ts +++ b/src/lib/settings/settingsTypes.ts @@ -4,6 +4,10 @@ import { AtLeast, PathValue, Paths } from 'lib/types' import { CommandArgumentConfig } from 'lib/commandTypes' import { Themes } from 'lib/theme' import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType' +import { + UnitAngle_type, + UnitLength_type, +} from '@kittycad/lib/dist/types/src/models' import { CameraOrbitType } from 'wasm-lib/kcl/bindings/CameraOrbitType' export interface SettingsViaQueryString { @@ -138,3 +142,12 @@ type RecursiveSettingsPayloads = { } export type SaveSettingsPayload = RecursiveSettingsPayloads + +/** + * Annotation names for default units are defined on rust side in + * src/wasm-lib/kcl/src/execution/annotations.rs + */ +export interface KclSettingsAnnotation { + defaultLengthUnit?: UnitLength_type + defaultAngleUnit?: UnitAngle_type +} diff --git a/src/lib/wasm_lib_wrapper.ts b/src/lib/wasm_lib_wrapper.ts index 93d670e7f..e6b1b3800 100644 --- a/src/lib/wasm_lib_wrapper.ts +++ b/src/lib/wasm_lib_wrapper.ts @@ -26,6 +26,7 @@ import { default_project_settings as DefaultProjectSettings, base64_decode as Base64Decode, clear_scene_and_bust_cache as ClearSceneAndBustCache, + kcl_settings as KclSettings, change_kcl_settings as ChangeKclSettings, } from '../wasm-lib/pkg/wasm_lib' @@ -111,6 +112,9 @@ export const clear_scene_and_bust_cache: typeof ClearSceneAndBustCache = ( ) => { return getModule().clear_scene_and_bust_cache(...args) } +export const kcl_settings: typeof KclSettings = (...args) => { + return getModule().kcl_settings(...args) +} export const change_kcl_settings: typeof ChangeKclSettings = (...args) => { return getModule().change_kcl_settings(...args) } diff --git a/src/wasm-lib/kcl/src/lib.rs b/src/wasm-lib/kcl/src/lib.rs index 02b75fe1c..8ac2981b6 100644 --- a/src/wasm-lib/kcl/src/lib.rs +++ b/src/wasm-lib/kcl/src/lib.rs @@ -159,7 +159,7 @@ impl Program { } /// Get the meta settings for the kcl file from the annotations. - pub fn get_meta_settings(&self) -> Result, KclError> { + pub fn meta_settings(&self) -> Result, KclError> { self.ast.get_meta_settings() } diff --git a/src/wasm-lib/src/wasm.rs b/src/wasm-lib/src/wasm.rs index 48a7015b7..0fe2ca3bc 100644 --- a/src/wasm-lib/src/wasm.rs +++ b/src/wasm-lib/src/wasm.rs @@ -539,6 +539,18 @@ pub fn calculate_circle_from_3_points(ax: f64, ay: f64, bx: f64, by: f64, cx: f6 } } +/// Takes a parsed KCL program and returns the Meta settings. If it's not +/// found, null is returned. +#[wasm_bindgen] +pub fn kcl_settings(program_json: &str) -> Result { + console_error_panic_hook::set_once(); + + let program: Program = serde_json::from_str(program_json).map_err(|e| e.to_string())?; + let settings = program.meta_settings().map_err(|e| e.to_string())?; + + JsValue::from_serde(&settings).map_err(|e| e.to_string()) +} + /// Takes a kcl string and Meta settings and changes the meta settings in the kcl string. #[wasm_bindgen] pub fn change_kcl_settings(code: &str, settings_str: &str) -> Result {