Update lower-right corner units menu to read and edit inline settings annotations if present (#5212)
* WIP show annotation length unit setting in LowerRightControls if present * Add logic for changing settings annotation if it's present * Add E2E test * Cleanup lints, fmt, tsc, logs * Change to use settings from Rust helper function - Fix thrown error to use the cause field - Fix function names to not use "get" * Remove unneeded constants * Post-merge fixups * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * Add back `ImportStatement` to make tsc happy (thanks @jtran!) * fmt --------- Co-authored-by: Jonathan Tran <jonnytran@gmail.com> Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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 (
|
||||
<Popover className="relative pointer-events-auto">
|
||||
{({ close }) => (
|
||||
@ -18,7 +40,7 @@ export function UnitsMenu() {
|
||||
<div className="absolute w-[1px] h-[1em] bg-primary right-0 top-1/2 -translate-y-1/2"></div>
|
||||
</div>
|
||||
<span className="sr-only">Current units are: </span>
|
||||
{settings.context.modeling.defaultUnit.current}
|
||||
{lengthSetting}
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
className={`absolute bottom-full right-0 mb-2 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
|
||||
@ -31,18 +53,40 @@ export function UnitsMenu() {
|
||||
<button
|
||||
className="flex items-center gap-2 m-0 py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
|
||||
onClick={() => {
|
||||
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()
|
||||
}}
|
||||
>
|
||||
<span className="flex-1">{baseUnitLabels[unit]}</span>
|
||||
{unit === settings.context.modeling.defaultUnit.current && (
|
||||
{unit === lengthSetting && (
|
||||
<span className="text-chalkboard-60">current</span>
|
||||
)}
|
||||
</button>
|
||||
|
@ -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<Program>
|
||||
@ -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<Program>) {
|
||||
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[] = [
|
||||
|
@ -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<Program>
|
||||
): 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
|
||||
}
|
||||
|
@ -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<Program>
|
||||
): MetaSettings | null | Error {
|
||||
let program: Node<Program>
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
@ -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<T> = {
|
||||
}
|
||||
|
||||
export type SaveSettingsPayload = RecursiveSettingsPayloads<typeof settings>
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ impl Program {
|
||||
}
|
||||
|
||||
/// Get the meta settings for the kcl file from the annotations.
|
||||
pub fn get_meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
|
||||
pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
|
||||
self.ast.get_meta_settings()
|
||||
}
|
||||
|
||||
|
@ -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<JsValue, String> {
|
||||
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<String, String> {
|
||||
|
Reference in New Issue
Block a user