Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2025-01-05 18:57:10 -08:00
parent 3c53babb50
commit 09ef1d5f10
39 changed files with 388 additions and 349 deletions

View File

@ -12,9 +12,10 @@ import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture' import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture' import { ToolbarFixture } from './toolbarFixture'
import { SceneFixture } from './sceneFixture' import { SceneFixture } from './sceneFixture'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { HomePageFixture } from './homePageFixture' import { HomePageFixture } from './homePageFixture'
import { unsafeTypedKeys } from 'lib/utils' import { unsafeTypedKeys } from 'lib/utils'
import { DeepPartial } from 'lib/types'
import { Settings } from 'wasm-lib/kcl/bindings/Settings'
export class AuthenticatedApp { export class AuthenticatedApp {
public readonly page: Page public readonly page: Page
@ -78,7 +79,7 @@ export class AuthenticatedTronApp {
fixtures: Partial<Fixtures> fixtures: Partial<Fixtures>
folderSetupFn?: (projectDirName: string) => Promise<void> folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload> appSettings?: DeepPartial<Settings>
} = { fixtures: {} } } = { fixtures: {} }
) { ) {
const { electronApp, page, context, dir } = await setupElectron({ const { electronApp, page, context, dir } = await setupElectron({

View File

@ -1,7 +1,12 @@
import { test, expect } from './zoo-test' import { test, expect } from './zoo-test'
import { join } from 'path' import { join } from 'path'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { getUtils, executorInputPath, createProject } from './test-utils' import {
getUtils,
executorInputPath,
createProject,
settingsToToml,
} from './test-utils'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { import {
@ -10,7 +15,6 @@ import {
TEST_SETTINGS_ONBOARDING_EXPORT, TEST_SETTINGS_ONBOARDING_EXPORT,
TEST_SETTINGS_ONBOARDING_USER_MENU, TEST_SETTINGS_ONBOARDING_USER_MENU,
} from './storageStates' } from './storageStates'
import * as TOML from '@iarna/toml'
import { expectPixelColor } from './fixtures/sceneFixture' import { expectPixelColor } from './fixtures/sceneFixture'
// Because onboarding relies on an app setting we need to set it as incompletel // Because onboarding relies on an app setting we need to set it as incompletel
@ -22,7 +26,7 @@ test.describe('Onboarding tests', () => {
{ {
appSettings: { appSettings: {
app: { app: {
onboardingStatus: 'incomplete', onboarding_status: 'incomplete',
}, },
}, },
cleanProjectDir: true, cleanProjectDir: true,
@ -63,7 +67,7 @@ test.describe('Onboarding tests', () => {
tag: '@electron', tag: '@electron',
appSettings: { appSettings: {
app: { app: {
onboardingStatus: 'incomplete', onboarding_status: 'incomplete',
}, },
}, },
cleanProjectDir: true, cleanProjectDir: true,
@ -108,7 +112,7 @@ test.describe('Onboarding tests', () => {
{ {
appSettings: { appSettings: {
app: { app: {
onboardingStatus: 'incomplete', onboarding_status: 'incomplete',
}, },
}, },
cleanProjectDir: true, cleanProjectDir: true,
@ -158,7 +162,7 @@ test.describe('Onboarding tests', () => {
{ {
appSettings: { appSettings: {
app: { app: {
onboardingStatus: 'incomplete', onboarding_status: 'incomplete',
}, },
}, },
}, },
@ -172,7 +176,7 @@ test.describe('Onboarding tests', () => {
}, },
{ {
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_START, settings: TEST_SETTINGS_ONBOARDING_START,
}), }),
} }
@ -208,7 +212,7 @@ test.describe('Onboarding tests', () => {
{ {
appSettings: { appSettings: {
app: { app: {
onboardingStatus: '/export', onboarding_status: '/export',
}, },
}, },
cleanProjectDir: true, cleanProjectDir: true,
@ -225,7 +229,7 @@ test.describe('Onboarding tests', () => {
}, },
{ {
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT, settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}), }),
} }
@ -263,7 +267,7 @@ test.describe('Onboarding tests', () => {
{ {
appSettings: { appSettings: {
app: { app: {
onboardingStatus: '/parametric-modeling', onboarding_status: '/parametric-modeling',
}, },
}, },
cleanProjectDir: true, cleanProjectDir: true,
@ -313,7 +317,7 @@ test.describe('Onboarding tests', () => {
{ {
appSettings: { appSettings: {
app: { app: {
onboardingStatus: 'incomplete', onboarding_status: 'incomplete',
}, },
}, },
cleanProjectDir: true, cleanProjectDir: true,
@ -326,7 +330,7 @@ test.describe('Onboarding tests', () => {
}, },
{ {
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU, settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}), }),
} }
@ -386,7 +390,7 @@ test.describe('Onboarding tests', () => {
{ {
appSettings: { appSettings: {
app: { app: {
onboardingStatus: 'incomplete', onboarding_status: 'incomplete',
}, },
}, },
cleanProjectDir: true, cleanProjectDir: true,
@ -400,7 +404,7 @@ test.describe('Onboarding tests', () => {
}, },
{ {
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_USER_MENU, settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
}), }),
} }
@ -442,7 +446,7 @@ test.fixme(
{ {
appSettings: { appSettings: {
app: { app: {
onboardingStatus: 'dismissed', onboarding_status: 'dismissed',
}, },
}, },
cleanProjectDir: true, cleanProjectDir: true,

View File

@ -1,6 +1,6 @@
import { test, expect } from '@playwright/test' import { test, expect } from '@playwright/test'
import { secrets } from './secrets' import { secrets } from './secrets'
import { Paths, doExport, getUtils } from './test-utils' import { Paths, doExport, getUtils, settingsToToml } from './test-utils'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { spawn } from 'child_process' import { spawn } from 'child_process'
@ -12,7 +12,6 @@ import {
TEST_SETTINGS, TEST_SETTINGS,
TEST_SETTINGS_KEY, TEST_SETTINGS_KEY,
} from './storageStates' } from './storageStates'
import * as TOML from '@iarna/toml'
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// reducedMotion kills animations, which speeds up tests and reduces flakiness // reducedMotion kills animations, which speeds up tests and reduces flakiness
@ -29,7 +28,7 @@ test.beforeEach(async ({ page }) => {
{ {
token: secrets.token, token: secrets.token,
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: TEST_SETTINGS }), settings: settingsToToml({ settings: TEST_SETTINGS }),
IS_PLAYWRIGHT_KEY: IS_PLAYWRIGHT_KEY, IS_PLAYWRIGHT_KEY: IS_PLAYWRIGHT_KEY,
} }
) )
@ -704,12 +703,12 @@ test.describe(
}, },
{ {
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: settingsToToml({
settings: { settings: {
...TEST_SETTINGS, ...TEST_SETTINGS,
modeling: { modeling: {
...TEST_SETTINGS.modeling, ...TEST_SETTINGS.modeling,
defaultUnit: 'mm', base_unit: 'mm',
}, },
}, },
}), }),
@ -1062,12 +1061,12 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
}, },
{ {
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: settingsToToml({
settings: { settings: {
...TEST_SETTINGS, ...TEST_SETTINGS,
modeling: { modeling: {
...TEST_SETTINGS.modeling, ...TEST_SETTINGS.modeling,
showScaleGrid: true, show_scale_grid: true,
}, },
}, },
}), }),

View File

@ -1,78 +1,83 @@
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { DeepPartial } from 'lib/types'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { Settings } from 'wasm-lib/kcl/bindings/Settings'
export const IS_PLAYWRIGHT_KEY = 'playwright' export const IS_PLAYWRIGHT_KEY = 'playwright'
export const TEST_SETTINGS_KEY = '/settings.toml' export const TEST_SETTINGS_KEY = '/settings.toml'
export const TEST_SETTINGS = { export const TEST_SETTINGS = {
app: { app: {
onboarding_status: 'dismissed',
appearance: {
theme: Themes.Dark, theme: Themes.Dark,
onboardingStatus: 'dismissed', },
projectDirectory: '',
enableSSAO: false,
}, },
modeling: { modeling: {
defaultUnit: 'in', base_unit: 'in',
mouseControls: 'Zoo', mouse_controls: 'zoo',
cameraProjection: 'perspective', camera_projection: 'perspective',
showDebugPanel: true, show_debug_panel: true,
enable_ssao: false,
}, },
projects: { project: {
defaultProjectName: 'project-$nnn', default_project_name: 'project-$nnn',
directory: '',
}, },
textEditor: { text_editor: {
textWrapping: true, text_wrapping: true,
}, },
} satisfies Partial<SaveSettingsPayload> } satisfies DeepPartial<Settings>
export const TEST_SETTINGS_ONBOARDING_USER_MENU = { export const TEST_SETTINGS_ONBOARDING_USER_MENU = {
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboardingStatus: onboardingPaths.USER_MENU }, app: { ...TEST_SETTINGS.app, onboarding_status: onboardingPaths.USER_MENU },
} satisfies Partial<SaveSettingsPayload> } satisfies DeepPartial<Settings>
export const TEST_SETTINGS_ONBOARDING_EXPORT = { export const TEST_SETTINGS_ONBOARDING_EXPORT = {
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboardingStatus: onboardingPaths.EXPORT }, app: { ...TEST_SETTINGS.app, onboarding_status: onboardingPaths.EXPORT },
} satisfies Partial<SaveSettingsPayload> } satisfies DeepPartial<Settings>
export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING = { export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING = {
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { app: {
...TEST_SETTINGS.app, ...TEST_SETTINGS.app,
onboardingStatus: onboardingPaths.PARAMETRIC_MODELING, onboarding_status: onboardingPaths.PARAMETRIC_MODELING,
}, },
} satisfies Partial<SaveSettingsPayload> } satisfies DeepPartial<Settings>
export const TEST_SETTINGS_ONBOARDING_START = { export const TEST_SETTINGS_ONBOARDING_START = {
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboardingStatus: '' }, app: { ...TEST_SETTINGS.app, onboarding_status: '' },
} satisfies Partial<SaveSettingsPayload> } satisfies DeepPartial<Settings>
export const TEST_SETTINGS_DEFAULT_THEME = { export const TEST_SETTINGS_DEFAULT_THEME = {
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, theme: Themes.System }, app: { ...TEST_SETTINGS.app, appearance: { theme: Themes.System } },
} satisfies Partial<SaveSettingsPayload> } satisfies DeepPartial<Settings>
export const TEST_SETTINGS_CORRUPTED = { export const TEST_SETTINGS_CORRUPTED = {
app: { app: {
onboarding_status: 'dismissed',
appearance: {
theme: Themes.Dark, theme: Themes.Dark,
onboardingStatus: 'dismissed', },
projectDirectory: 123 as any,
}, },
modeling: { modeling: {
defaultUnit: 'invalid' as any, base_unit: 'invalid' as any,
mouseControls: `() => alert('hack the planet')` as any, mouse_controls: `() => alert('hack the planet')` as any,
cameraProjection: 'perspective', camera_projection: 'perspective',
showDebugPanel: true, show_debug_panel: true,
}, },
projects: { project: {
defaultProjectName: false as any, default_project_name: false as any,
directory: 123 as any,
}, },
textEditor: { text_editor: {
textWrapping: true, text_wrapping: true,
}, },
} satisfies Partial<SaveSettingsPayload> } satisfies DeepPartial<Settings>
export const TEST_CODE_GIZMO = `part001 = startSketchOn('XZ') export const TEST_CODE_GIZMO = `part001 = startSketchOn('XZ')
|> startProfileAt([20, 0], %) |> startProfileAt([20, 0], %)

View File

@ -23,11 +23,13 @@ import {
IS_PLAYWRIGHT_KEY, IS_PLAYWRIGHT_KEY,
} from './storageStates' } from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants' import { SETTINGS_FILE_NAME } from 'lib/constants'
import { isErrorWhitelisted } from './lib/console-error-whitelist' import { isErrorWhitelisted } from './lib/console-error-whitelist'
import { isArray } from 'lib/utils' import { isArray } from 'lib/utils'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { Configuration } from 'lang/wasm'
import { DeepPartial } from 'lib/types'
import { Settings } from 'wasm-lib/kcl/bindings/Settings'
const toNormalizedCode = (text: string) => { const toNormalizedCode = (text: string) => {
return text.replace(/\s+/g, '') return text.replace(/\s+/g, '')
@ -884,19 +886,23 @@ export async function setup(
{ {
token: secrets.token, token: secrets.token,
settingsKey: TEST_SETTINGS_KEY, settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({ settings: settingsToToml({
settings: { settings: {
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { app: {
...TEST_SETTINGS.projects, ...TEST_SETTINGS.app,
projectDirectory: TEST_SETTINGS.app.projectDirectory, onboarding_status: 'dismissed',
onboardingStatus: 'dismissed', appearance: {
theme: 'dark', theme: 'dark',
}, },
} as Partial<SaveSettingsPayload>, },
project: {
directory: TEST_SETTINGS.project.directory,
},
},
}), }),
IS_PLAYWRIGHT_KEY, IS_PLAYWRIGHT_KEY,
PLAYWRIGHT_TEST_DIR: TEST_SETTINGS.app.projectDirectory, PLAYWRIGHT_TEST_DIR: TEST_SETTINGS.project.directory,
PERSIST_MODELING_CONTEXT, PERSIST_MODELING_CONTEXT,
} }
) )
@ -931,7 +937,7 @@ export async function setupElectron({
testInfo: TestInfo testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void> folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload> appSettings?: DeepPartial<Settings>
}): Promise<{ }): Promise<{
electronApp: ElectronApplication electronApp: ElectronApplication
context: BrowserContext context: BrowserContext
@ -978,7 +984,7 @@ export async function setupElectron({
if (cleanProjectDir) { if (cleanProjectDir) {
const tempSettingsFilePath = path.join(projectDirName, SETTINGS_FILE_NAME) const tempSettingsFilePath = path.join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = TOML.stringify( const settingsOverrides = settingsToToml(
appSettings appSettings
? { ? {
settings: { settings: {
@ -986,9 +992,11 @@ export async function setupElectron({
...appSettings, ...appSettings,
app: { app: {
...TEST_SETTINGS.app, ...TEST_SETTINGS.app,
projectDirectory: projectDirName,
...appSettings.app, ...appSettings.app,
}, },
project: {
directory: projectDirName,
},
}, },
} }
: { : {
@ -996,7 +1004,9 @@ export async function setupElectron({
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { app: {
...TEST_SETTINGS.app, ...TEST_SETTINGS.app,
projectDirectory: projectDirName, },
project: {
directory: projectDirName,
}, },
}, },
} }
@ -1190,3 +1200,7 @@ export async function pollEditorLinesSelectedLength(page: Page, lines: number) {
}) })
.toBe(lines) .toBe(lines)
} }
export function settingsToToml(settings: DeepPartial<Configuration>) {
return TOML.stringify(settings)
}

View File

@ -31,13 +31,13 @@ test.describe('Testing settings', () => {
) )
) as { settings: SaveSettingsPayload } ) as { settings: SaveSettingsPayload }
expect(storedSettings.settings?.app?.theme).toBe('dark') expect(storedSettings.settings?.app?.appearance?.theme).toBe('dark')
// Check that the invalid settings were changed to good defaults // Check that the invalid settings were changed to good defaults
expect(storedSettings.settings?.modeling?.defaultUnit).toBe('in') expect(storedSettings.settings?.modeling?.base_unit).toBe('in')
expect(storedSettings.settings?.modeling?.mouseControls).toBe('Zoo') expect(storedSettings.settings?.modeling?.mouse_controls).toBe('Zoo')
expect(storedSettings.settings?.app?.projectDirectory).toBe('') expect(storedSettings.settings?.project?.directory).toBe('')
expect(storedSettings.settings?.projects?.defaultProjectName).toBe( expect(storedSettings.settings?.project?.default_project_name).toBe(
'project-$nnn' 'project-$nnn'
) )
} }
@ -374,7 +374,9 @@ test.describe('Testing settings', () => {
tag: '@electron', tag: '@electron',
appSettings: { appSettings: {
app: { app: {
themeColor: '259', appearance: {
color: 259,
},
}, },
}, },
}, },
@ -400,9 +402,11 @@ test.describe('Testing settings', () => {
tag: '@electron', tag: '@electron',
appSettings: { appSettings: {
app: { app: {
appearance: {
// Doesn't matter what you set it to. It will // Doesn't matter what you set it to. It will
// default to 264.5 // default to 264.5
themeColor: '0', color: 0,
},
}, },
}, },
}, },
@ -832,7 +836,7 @@ test.describe('Testing settings', () => {
// but "show debug panel" set to false // but "show debug panel" set to false
appSettings: { appSettings: {
...TEST_SETTINGS, ...TEST_SETTINGS,
modeling: { ...TEST_SETTINGS.modeling, showDebugPanel: false }, modeling: { ...TEST_SETTINGS.modeling, show_debug_panel: false },
}, },
}, },
async ({ context, page, homePage }) => { async ({ context, page, homePage }) => {

View File

@ -18,7 +18,8 @@ import {
AuthenticatedApp, AuthenticatedApp,
} from './fixtures/fixtureSetup' } from './fixtures/fixtureSetup'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes' import { Settings } from 'wasm-lib/kcl/bindings/Settings'
import { DeepPartial } from 'lib/types'
export { expect } from '@playwright/test' export { expect } from '@playwright/test'
declare module '@playwright/test' { declare module '@playwright/test' {
@ -45,7 +46,7 @@ export type BrowserContext = BrowserContextPlaywright
export type Page = PagePlaywright export type Page = PagePlaywright
export type TestDetails = TestDetailsPlaywright & { export type TestDetails = TestDetailsPlaywright & {
cleanProjectDir?: boolean cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload> appSettings?: DeepPartial<Settings>
} }
// Our custom decorated Zoo test object. Makes it easier to add fixtures, and // Our custom decorated Zoo test object. Makes it easier to add fixtures, and

View File

@ -54,7 +54,7 @@ export function App() {
) )
const { const {
app: { onboardingStatus }, app: { onboarding_status },
} = settings.context } = settings.context
useHotkeys('backspace', (e) => { useHotkeys('backspace', (e) => {
@ -69,7 +69,7 @@ export function App() {
) )
const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some( const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
(p) => p === onboardingStatus.current (p) => p === onboarding_status.current
) )
? 'opacity-20' ? 'opacity-20'
: '' : ''

View File

@ -77,7 +77,7 @@ export const ClientSideScene = ({
}: { }: {
cameraControls: ReturnType< cameraControls: ReturnType<
typeof useSettingsAuthContext typeof useSettingsAuthContext
>['settings']['context']['modeling']['mouseControls']['current'] >['settings']['context']['modeling']['mouse_controls']['current']
}) => { }) => {
const canvasRef = useRef<HTMLDivElement>(null) const canvasRef = useRef<HTMLDivElement>(null)
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()

View File

@ -5,21 +5,21 @@ import { useEffect, useState } from 'react'
export function CameraProjectionToggle() { export function CameraProjectionToggle() {
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const isCameraProjectionPerspective = const isCameraProjectionPerspective =
settings.context.modeling.cameraProjection.current === 'perspective' settings.context.modeling.camera_projection.current === 'perspective'
const [checked, setChecked] = useState(isCameraProjectionPerspective) const [checked, setChecked] = useState(isCameraProjectionPerspective)
useEffect(() => { useEffect(() => {
setChecked( setChecked(
settings.context.modeling.cameraProjection.current === 'perspective' settings.context.modeling.camera_projection.current === 'perspective'
) )
}, [settings.context.modeling.cameraProjection.current]) }, [settings.context.modeling.camera_projection.current])
return ( return (
<Switch <Switch
checked={checked} checked={checked}
onChange={(newValue) => { onChange={(newValue) => {
settings.send({ settings.send({
type: 'set.modeling.cameraProjection', type: 'set.modeling.camera_projection',
data: { data: {
level: 'user', level: 'user',
value: newValue ? 'perspective' : 'orthographic', value: newValue ? 'perspective' : 'orthographic',

View File

@ -115,9 +115,9 @@ function CommandBarKclInput({
: defaultValue.length, : defaultValue.length,
}, },
theme: theme:
settings.context.app.theme.current === 'system' settings.context.app.appearance.theme.current === 'system'
? getSystemTheme() ? getSystemTheme()
: settings.context.app.theme.current, : settings.context.app.appearance.theme.current,
extensions: [ extensions: [
varMentionsExtension, varMentionsExtension,
EditorView.updateListener.of((vu: ViewUpdate) => { EditorView.updateListener.of((vu: ViewUpdate) => {

View File

@ -6,7 +6,7 @@ import { useState } from 'react'
const DownloadAppBanner = () => { const DownloadAppBanner = () => {
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const [isBannerDismissed, setIsBannerDismissed] = useState( const [isBannerDismissed, setIsBannerDismissed] = useState(
settings.context.app.dismissWebBanner.current settings.context.app.dismiss_web_banner.current
) )
return ( return (

View File

@ -315,7 +315,7 @@ export const FileMachineProvider = ({
// with the sample's setting. // with the sample's setting.
if (data.sampleUnits) { if (data.sampleUnits) {
settings.send({ settings.send({
type: 'set.modeling.defaultUnit', type: 'set.modeling.base_unit',
data: { data: {
level: 'project', level: 'project',
value: data.sampleUnits, value: data.sampleUnits,

View File

@ -107,7 +107,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
as="button" as="button"
onClick={() => { onClick={() => {
settings.send({ settings.send({
type: 'set.app.onboardingStatus', type: 'set.app.onboarding_status',
data: { data: {
value: '', value: '',
level: 'user', level: 'user',

View File

@ -111,12 +111,15 @@ export const ModelingMachineProvider = ({
auth, auth,
settings: { settings: {
context: { context: {
app: { theme, enableSSAO }, app: {
appearance: { theme },
},
modeling: { modeling: {
defaultUnit, base_unit,
cameraProjection, camera_projection,
highlightEdges, highlight_edges,
showScaleGrid, show_scale_grid,
enable_ssao,
}, },
}, },
}, },
@ -172,7 +175,7 @@ export const ModelingMachineProvider = ({
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') { if (camera_projection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine() await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
} }
@ -508,7 +511,7 @@ export const ModelingMachineProvider = ({
format.type === 'stl' || format.type === 'stl' ||
format.type === 'ply' format.type === 'ply'
) { ) {
format.units = defaultUnit.current format.units = base_unit.current
} }
if (format.type === 'ply' || format.type === 'stl') { if (format.type === 'ply' || format.type === 'stl') {
@ -533,7 +536,7 @@ export const ModelingMachineProvider = ({
token, token,
settings: { settings: {
theme: theme.current, theme: theme.current,
highlightEdges: highlightEdges.current, highlightEdges: highlight_edges.current,
}, },
}).catch(reportRejection) }).catch(reportRejection)
}, },
@ -1134,10 +1137,10 @@ export const ModelingMachineProvider = ({
{ {
pool: pool, pool: pool,
theme: theme.current, theme: theme.current,
highlightEdges: highlightEdges.current, highlightEdges: highlight_edges.current,
enableSSAO: enableSSAO.current, enableSSAO: enable_ssao.current,
showScaleGrid: showScaleGrid.current, showScaleGrid: show_scale_grid.current,
cameraProjection: cameraProjection.current, cameraProjection: camera_projection.current,
}, },
token token
) )

View File

@ -69,7 +69,7 @@ export const ModelingPane = ({
...props ...props
}: ModelingPaneProps) => { }: ModelingPaneProps) => {
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus const onboardingStatus = settings.context.app.onboarding_status
const pointerEventsCssClass = const pointerEventsCssClass =
onboardingStatus.current === onboardingPaths.CAMERA onboardingStatus.current === onboardingPaths.CAMERA
? 'pointer-events-none ' ? 'pointer-events-none '

View File

@ -69,9 +69,9 @@ export const KclEditorPane = () => {
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector) const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
const editorIsMounted = useSelector(kclEditorActor, editorIsMountedSelector) const editorIsMounted = useSelector(kclEditorActor, editorIsMountedSelector)
const theme = const theme =
context.app.theme.current === Themes.System context.app.appearance.theme.current === Themes.System
? getSystemTheme() ? getSystemTheme()
: context.app.theme.current : context.app.appearance.theme.current
const { copilotLSP, kclLSP } = useLspContext() const { copilotLSP, kclLSP } = useLspContext()
// Since these already exist in the editor, we don't need to define them // Since these already exist in the editor, we don't need to define them
@ -104,8 +104,8 @@ export const KclEditorPane = () => {
}) })
}, [editorIsMounted, lastSelectionEvent]) }, [editorIsMounted, lastSelectionEvent])
const textWrapping = context.textEditor.textWrapping const textWrapping = context.text_editor.text_wrapping
const cursorBlinking = context.textEditor.blinkingCursor const cursorBlinking = context.text_editor.blinking_cursor
// DO NOT ADD THE CODEMIRROR HOTKEYS HERE TO THE DEPENDENCY ARRAY // DO NOT ADD THE CODEMIRROR HOTKEYS HERE TO THE DEPENDENCY ARRAY
// It reloads the editor every time we do _anything_ in the editor // It reloads the editor every time we do _anything_ in the editor
// I have no idea why. // I have no idea why.

View File

@ -210,6 +210,6 @@ export const sidebarPanes: SidebarPane[] = [
) )
}, },
keybinding: 'Shift + D', keybinding: 'Shift + D',
hide: ({ settings }) => !settings.modeling.showDebugPanel.current, hide: ({ settings }) => !settings.modeling.show_debug_panel.current,
}, },
] ]

View File

@ -40,14 +40,14 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext() const kclContext = useKclContext()
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus const onboardingStatus = settings.context.app.onboarding_status
const { send, context } = useModelingContext() const { send, context } = useModelingContext()
const pointerEventsCssClass = const pointerEventsCssClass =
onboardingStatus.current === onboardingPaths.CAMERA || onboardingStatus.current === onboardingPaths.CAMERA ||
context.store?.openPanes.length === 0 context.store?.openPanes.length === 0
? 'pointer-events-none ' ? 'pointer-events-none '
: 'pointer-events-auto ' : 'pointer-events-auto '
const showDebugPanel = settings.context.modeling.showDebugPanel const showDebugPanel = settings.context.modeling.show_debug_panel
const paneCallbackProps = useMemo( const paneCallbackProps = useMemo(
() => ({ () => ({

View File

@ -79,11 +79,8 @@ const ProjectsContextDesktop = ({
} = useSettingsAuthContext() } = useSettingsAuthContext()
useEffect(() => { useEffect(() => {
console.log( console.log('project directory changed', settings.project.directory.current)
'project directory changed', }, [settings.project.directory.current])
settings.app.projectDirectory.current
)
}, [settings.app.projectDirectory.current])
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0) const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectPaths, projectsDir } = useProjectsLoader([ const { projectPaths, projectsDir } = useProjectsLoader([
@ -192,7 +189,7 @@ const ProjectsContextDesktop = ({
let name = ( let name = (
input && 'name' in input && input.name input && 'name' in input && input.name
? input.name ? input.name
: settings.projects.defaultProjectName.current : settings.project.default_project_name.current
).trim() ).trim()
if (doesProjectNameNeedInterpolated(name)) { if (doesProjectNameNeedInterpolated(name)) {
@ -257,8 +254,8 @@ const ProjectsContextDesktop = ({
{ {
input: { input: {
projects: projectPaths, projects: projectPaths,
defaultProjectName: settings.projects.defaultProjectName.current, defaultProjectName: settings.project.default_project_name.current,
defaultDirectory: settings.app.projectDirectory.current, defaultDirectory: settings.project.directory.current,
}, },
} }
) )

View File

@ -61,7 +61,7 @@ export const AllSettingsFields = forwardRef(
function restartOnboarding() { function restartOnboarding() {
send({ send({
type: `set.app.onboardingStatus`, type: `set.app.onboarding_status`,
data: { level: 'user', value: '' }, data: { level: 'user', value: '' },
}) })
} }
@ -73,7 +73,7 @@ export const AllSettingsFields = forwardRef(
useEffect(() => { useEffect(() => {
async function navigateToOnboardingStart() { async function navigateToOnboardingStart() {
if ( if (
state.context.app.onboardingStatus.user === '' && state.context.app.onboarding_status.user === '' &&
state.matches('idle') state.matches('idle')
) { ) {
if (isFileSettings) { if (isFileSettings) {

View File

@ -122,18 +122,20 @@ export const SettingsAuthProviderBase = ({
setClientSideSceneUnits: ({ context, event }) => { setClientSideSceneUnits: ({ context, event }) => {
const newBaseUnit = const newBaseUnit =
event.type === 'set.modeling.defaultUnit' event.type === 'set.modeling.base_unit'
? (event.data.value as BaseUnit) ? (event.data.value as BaseUnit)
: context.modeling.defaultUnit.current : context.modeling.base_unit.current
sceneInfra.baseUnit = newBaseUnit sceneInfra.baseUnit = newBaseUnit
}, },
setEngineTheme: ({ context }) => { setEngineTheme: ({ context }) => {
engineCommandManager engineCommandManager
.setTheme(context.app.theme.current) .setTheme(context.app.appearance.theme.current)
.catch(reportRejection) .catch(reportRejection)
}, },
setClientTheme: ({ context }) => { setClientTheme: ({ context }) => {
const opposingTheme = getOppositeTheme(context.app.theme.current) const opposingTheme = getOppositeTheme(
context.app.appearance.theme.current
)
sceneInfra.theme = opposingTheme sceneInfra.theme = opposingTheme
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme) sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
}, },
@ -164,12 +166,12 @@ export const SettingsAuthProviderBase = ({
try { try {
const relevantSetting = (s: typeof settings) => { const relevantSetting = (s: typeof settings) => {
return ( return (
s.modeling?.defaultUnit?.current !== s.modeling?.base_unit.current !==
context.modeling.defaultUnit.current || context.modeling.base_unit.current ||
s.modeling.showScaleGrid.current !== s.modeling.show_scale_grid.current !==
context.modeling.showScaleGrid.current || context.modeling.show_scale_grid.current ||
s.modeling?.highlightEdges.current !== s.modeling?.highlight_edges.current !==
context.modeling.highlightEdges.current context.modeling.highlight_edges.current
) )
} }
@ -180,9 +182,9 @@ export const SettingsAuthProviderBase = ({
event.type === 'Reset settings' && relevantSetting(settings) event.type === 'Reset settings' && relevantSetting(settings)
if ( if (
event.type === 'set.modeling.defaultUnit' || event.type === 'set.modeling.base_unit' ||
event.type === 'set.modeling.showScaleGrid' || event.type === 'set.modeling.show_scale_grid' ||
event.type === 'set.modeling.highlightEdges' || event.type === 'set.modeling.highlight_edges' ||
allSettingsIncludesUnitChange || allSettingsIncludesUnitChange ||
resetSettingsIncludesUnitChange resetSettingsIncludesUnitChange
) { ) {
@ -258,7 +260,7 @@ export const SettingsAuthProviderBase = ({
useEffect(() => { useEffect(() => {
// If the user wants to hide the settings commands // If the user wants to hide the settings commands
//from the command bar don't add them. //from the command bar don't add them.
if (settingsState.context.commandBar.includeSettings.current === false) if (settingsState.context.command_bar.include_settings.current === false)
return return
const commands = settingsWithCommandConfigs(settingsState.context) const commands = settingsWithCommandConfigs(settingsState.context)
@ -334,7 +336,8 @@ export const SettingsAuthProviderBase = ({
// events outside of the machine that also depend on the machine's context // events outside of the machine that also depend on the machine's context
useEffect(() => { useEffect(() => {
const listener = (e: MediaQueryListEvent) => { const listener = (e: MediaQueryListEvent) => {
if (settingsState.context.app.theme.current !== 'system') return if (settingsState.context.app.appearance.theme.current !== 'system')
return
setThemeClass(e.matches ? Themes.Dark : Themes.Light) setThemeClass(e.matches ? Themes.Dark : Themes.Light)
} }
@ -349,9 +352,9 @@ export const SettingsAuthProviderBase = ({
useEffect(() => { useEffect(() => {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
`--primary-hue`, `--primary-hue`,
settingsState.context.app.themeColor.current settingsState.context.app.appearance.color.current
) )
}, [settingsState.context.app.themeColor.current]) }, [settingsState.context.app.appearance.color.current])
/** /**
* Update the --cursor-color CSS variable * Update the --cursor-color CSS variable
@ -360,11 +363,11 @@ export const SettingsAuthProviderBase = ({
useEffect(() => { useEffect(() => {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
`--cursor-color`, `--cursor-color`,
settingsState.context.textEditor.blinkingCursor.current settingsState.context.text_editor.blinking_cursor.current
? 'auto' ? 'auto'
: 'transparent' : 'transparent'
) )
}, [settingsState.context.textEditor.blinkingCursor.current]) }, [settingsState.context.text_editor.blinking_cursor.current])
// Auth machine setup // Auth machine setup
const [authState, authSend, authActor] = useMachine( const [authState, authSend, authActor] = useMachine(

View File

@ -41,7 +41,7 @@ export const Stream = () => {
const [streamState, setStreamState] = useState(StreamState.Unset) const [streamState, setStreamState] = useState(StreamState.Unset)
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const IDLE = settings.context.app.streamIdleMode.current const IDLE = settings.context.app.stream_idle_mode.current
const isNetworkOkay = const isNetworkOkay =
overallState === NetworkHealthState.Ok || overallState === NetworkHealthState.Ok ||
@ -342,7 +342,7 @@ export const Stream = () => {
id="video-stream" id="video-stream"
/> />
<ClientSideScene <ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current} cameraControls={settings.context.modeling.mouse_controls.current}
/> />
{(streamState === StreamState.Paused || {(streamState === StreamState.Paused ||
streamState === StreamState.Resuming) && ( streamState === StreamState.Resuming) && (

View File

@ -18,7 +18,7 @@ export function UnitsMenu() {
<div className="absolute w-[1px] h-[1em] bg-primary right-0 top-1/2 -translate-y-1/2"></div> <div className="absolute w-[1px] h-[1em] bg-primary right-0 top-1/2 -translate-y-1/2"></div>
</div> </div>
<span className="sr-only">Current units are:&nbsp;</span> <span className="sr-only">Current units are:&nbsp;</span>
{settings.context.modeling.defaultUnit.current} {settings.context.modeling.base_unit.current}
</Popover.Button> </Popover.Button>
<Popover.Panel <Popover.Panel
className={`absolute bottom-full right-0 mb-2 w-48 bg-chalkboard-10 dark:bg-chalkboard-90 className={`absolute bottom-full right-0 mb-2 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
@ -32,7 +32,7 @@ export function UnitsMenu() {
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" 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={() => { onClick={() => {
settings.send({ settings.send({
type: 'set.modeling.defaultUnit', type: 'set.modeling.base_unit',
data: { data: {
level: 'project', level: 'project',
value: unit, value: unit,
@ -42,7 +42,7 @@ export function UnitsMenu() {
}} }}
> >
<span className="flex-1">{baseUnitLabels[unit]}</span> <span className="flex-1">{baseUnitLabels[unit]}</span>
{unit === settings.context.modeling.defaultUnit.current && ( {unit === settings.context.modeling.base_unit.current && (
<span className="text-chalkboard-60">current</span> <span className="text-chalkboard-60">current</span>
)} )}
</button> </button>

View File

@ -10,7 +10,7 @@ export function useResolvedTheme() {
const { const {
settings: { context }, settings: { context },
} = useSettingsAuthContext() } = useSettingsAuthContext()
return context.app.theme.current === Themes.System return context.app.appearance.theme.current === Themes.System
? getSystemTheme() ? getSystemTheme()
: context.app.theme.current : context.app.appearance.theme.current
} }

View File

@ -63,9 +63,7 @@ export async function renameProjectDirectory(
export async function ensureProjectDirectoryExists( export async function ensureProjectDirectoryExists(
config: DeepPartial<Configuration> config: DeepPartial<Configuration>
): Promise<string | undefined> { ): Promise<string | undefined> {
const projectDir = const projectDir = config.settings?.project?.directory
config.settings?.app?.project_directory ||
config.settings?.project?.directory
if (!projectDir) { if (!projectDir) {
console.error('projectDir is falsey', config) console.error('projectDir is falsey', config)
return Promise.reject(new Error('projectDir is falsey')) return Promise.reject(new Error('projectDir is falsey'))
@ -520,8 +518,7 @@ export const readAppSettingsFile = async () => {
} }
const hasProjectDirectorySetting = const hasProjectDirectorySetting =
parsedAppConfig.settings?.project?.directory || parsedAppConfig.settings?.project?.directory
parsedAppConfig.settings?.app?.project_directory
if (hasProjectDirectorySetting) { if (hasProjectDirectorySetting) {
return parsedAppConfig return parsedAppConfig

View File

@ -86,8 +86,7 @@ export function kclCommands(
sampleName: data.sample, sampleName: data.sample,
code, code,
method: data.method, method: data.method,
sampleUnits: sampleUnits: projectSettingsPayload.modeling?.base_unit || 'mm',
projectSettingsPayload.modeling?.defaultUnit || 'mm',
} }
} }
) )

View File

@ -55,7 +55,7 @@ export const telemetryLoader: LoaderFunction = async ({
export const onboardingRedirectLoader: ActionFunction = async (args) => { export const onboardingRedirectLoader: ActionFunction = async (args) => {
const { settings } = await loadAndValidateSettings() const { settings } = await loadAndValidateSettings()
const onboardingStatus: OnboardingStatus = const onboardingStatus: OnboardingStatus =
settings.app.onboardingStatus.current || '' settings.app.onboarding_status.current || ''
const notEnRouteToOnboarding = !args.request.url.includes( const notEnRouteToOnboarding = !args.request.url.includes(
PATHS.ONBOARDING.INDEX PATHS.ONBOARDING.INDEX
) )

View File

@ -128,6 +128,7 @@ export function createSettings() {
/** /**
* The overall appearance of the app: light, dark, or system * The overall appearance of the app: light, dark, or system
*/ */
appearance: {
theme: new Setting<Themes>({ theme: new Setting<Themes>({
hideOnLevel: 'project', hideOnLevel: 'project',
defaultValue: Themes.System, defaultValue: Themes.System,
@ -148,7 +149,7 @@ export function createSettings() {
})), })),
}, },
}), }),
themeColor: new Setting<string>({ color: new Setting<string>({
defaultValue: '264.5', defaultValue: '264.5',
description: 'The hue of the primary theme color for the app', description: 'The hue of the primary theme color for the app',
validate: (v) => Number(v) >= 0 && Number(v) < 360, validate: (v) => Number(v) >= 0 && Number(v) < 360,
@ -172,17 +173,11 @@ export function createSettings() {
</div> </div>
), ),
}), }),
enableSSAO: new Setting<boolean>({ },
defaultValue: true,
description:
'Whether or not Screen Space Ambient Occlusion (SSAO) is enabled',
validate: (v) => typeof v === 'boolean',
hideOnPlatform: 'both', //for now
}),
/** /**
* Stream resource saving behavior toggle * Stream resource saving behavior toggle
*/ */
streamIdleMode: new Setting<boolean>({ stream_idle_mode: new Setting<boolean>({
defaultValue: false, defaultValue: false,
description: 'Toggle stream idling, saving bandwidth and battery', description: 'Toggle stream idling, saving bandwidth and battery',
validate: (v) => typeof v === 'boolean', validate: (v) => typeof v === 'boolean',
@ -190,7 +185,7 @@ export function createSettings() {
inputType: 'boolean', inputType: 'boolean',
}, },
}), }),
onboardingStatus: new Setting<OnboardingStatus>({ onboarding_status: new Setting<OnboardingStatus>({
defaultValue: '', defaultValue: '',
// TODO: this could be better but we don't have a TS side real enum // TODO: this could be better but we don't have a TS side real enum
// for this yet // for this yet
@ -198,62 +193,13 @@ export function createSettings() {
hideOnPlatform: 'both', hideOnPlatform: 'both',
}), }),
/** Permanently dismiss the banner warning to download the desktop app. */ /** Permanently dismiss the banner warning to download the desktop app. */
dismissWebBanner: new Setting<boolean>({ dismiss_web_banner: new Setting<boolean>({
defaultValue: false, defaultValue: false,
description: description:
'Permanently dismiss the banner warning to download the desktop app.', 'Permanently dismiss the banner warning to download the desktop app.',
validate: (v) => typeof v === 'boolean', validate: (v) => typeof v === 'boolean',
hideOnPlatform: 'desktop', hideOnPlatform: 'desktop',
}), }),
projectDirectory: new Setting<string>({
defaultValue: '',
description: 'The directory to save and load projects from',
hideOnLevel: 'project',
hideOnPlatform: 'web',
validate: (v) =>
typeof v === 'string' && (v.length > 0 || !isDesktop()),
Component: ({ value, updateValue }) => {
const inputRef = useRef<HTMLInputElement>(null)
return (
<div className="flex gap-4 p-1 border rounded-sm border-chalkboard-30">
<input
className="flex-grow text-xs px-2 bg-transparent"
value={value}
disabled
data-testid="project-directory-input"
ref={inputRef}
/>
<button
onClick={toSync(async () => {
// In desktop end-to-end tests we can't control the file picker,
// so we seed the new directory value in the element's dataset
const inputRefVal = inputRef.current?.dataset.testValue
if (
inputRef.current &&
inputRefVal &&
!Array.isArray(inputRefVal)
) {
updateValue(inputRefVal)
} else {
const newPath = await window.electron.open({
properties: ['openDirectory', 'createDirectory'],
defaultPath: value,
title: 'Choose a new project directory',
})
if (newPath.canceled) return
updateValue(newPath.filePaths[0])
}
}, reportRejection)}
className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
data-testid="project-directory-button"
>
<CustomIcon name="folder" className="w-5 h-5" />
<Tooltip position="top-right">Choose a folder</Tooltip>
</button>
</div>
)
},
}),
}, },
/** /**
* Settings that affect the behavior while modeling. * Settings that affect the behavior while modeling.
@ -262,7 +208,7 @@ export function createSettings() {
/** /**
* The default unit to use in modeling dimensions * The default unit to use in modeling dimensions
*/ */
defaultUnit: new Setting<BaseUnit>({ base_unit: new Setting<BaseUnit>({
defaultValue: 'mm', defaultValue: 'mm',
description: 'The default unit to use in modeling dimensions', description: 'The default unit to use in modeling dimensions',
validate: (v) => baseUnitsUnion.includes(v as BaseUnit), validate: (v) => baseUnitsUnion.includes(v as BaseUnit),
@ -285,7 +231,7 @@ export function createSettings() {
/** /**
* The controls for how to navigate the 3D view * The controls for how to navigate the 3D view
*/ */
mouseControls: new Setting<CameraSystem>({ mouse_controls: new Setting<CameraSystem>({
defaultValue: 'Zoo', defaultValue: 'Zoo',
description: 'The controls for how to navigate the 3D view', description: 'The controls for how to navigate the 3D view',
validate: (v) => cameraSystems.includes(v as CameraSystem), validate: (v) => cameraSystems.includes(v as CameraSystem),
@ -345,7 +291,7 @@ export function createSettings() {
/** /**
* Projection method applied to the 3D view, perspective or orthographic * Projection method applied to the 3D view, perspective or orthographic
*/ */
cameraProjection: new Setting<CameraProjectionType>({ camera_projection: new Setting<CameraProjectionType>({
defaultValue: 'orthographic', defaultValue: 'orthographic',
hideOnLevel: 'project', hideOnLevel: 'project',
description: description:
@ -372,10 +318,17 @@ export function createSettings() {
})), })),
}, },
}), }),
enable_ssao: new Setting<boolean>({
defaultValue: true,
description:
'Whether or not Screen Space Ambient Occlusion (SSAO) is enabled',
validate: (v) => typeof v === 'boolean',
hideOnPlatform: 'both', //for now
}),
/** /**
* Whether to highlight edges of 3D objects * Whether to highlight edges of 3D objects
*/ */
highlightEdges: new Setting<boolean>({ highlight_edges: new Setting<boolean>({
defaultValue: true, defaultValue: true,
description: 'Whether to highlight edges of 3D objects', description: 'Whether to highlight edges of 3D objects',
validate: (v) => typeof v === 'boolean', validate: (v) => typeof v === 'boolean',
@ -387,7 +340,7 @@ export function createSettings() {
/** /**
* Whether to show a scale grid in the 3D modeling view * Whether to show a scale grid in the 3D modeling view
*/ */
showScaleGrid: new Setting<boolean>({ show_scale_grid: new Setting<boolean>({
defaultValue: false, defaultValue: false,
description: 'Whether to show a scale grid in the 3D modeling view', description: 'Whether to show a scale grid in the 3D modeling view',
validate: (v) => typeof v === 'boolean', validate: (v) => typeof v === 'boolean',
@ -400,7 +353,7 @@ export function createSettings() {
* Whether to show the debug panel, which lets you see * Whether to show the debug panel, which lets you see
* various states of the app to aid in development * various states of the app to aid in development
*/ */
showDebugPanel: new Setting<boolean>({ show_debug_panel: new Setting<boolean>({
defaultValue: false, defaultValue: false,
description: 'Whether to show the debug panel, a development tool', description: 'Whether to show the debug panel, a development tool',
validate: (v) => typeof v === 'boolean', validate: (v) => typeof v === 'boolean',
@ -438,11 +391,11 @@ export function createSettings() {
/** /**
* Settings that affect the behavior of the KCL text editor. * Settings that affect the behavior of the KCL text editor.
*/ */
textEditor: { text_editor: {
/** /**
* Whether to wrap text in the editor or overflow with scroll * Whether to wrap text in the editor or overflow with scroll
*/ */
textWrapping: new Setting<boolean>({ text_wrapping: new Setting<boolean>({
defaultValue: true, defaultValue: true,
description: description:
'Whether to wrap text in the editor or overflow with scroll', 'Whether to wrap text in the editor or overflow with scroll',
@ -454,7 +407,7 @@ export function createSettings() {
/** /**
* Whether to make the cursor blink in the editor * Whether to make the cursor blink in the editor
*/ */
blinkingCursor: new Setting<boolean>({ blinking_cursor: new Setting<boolean>({
defaultValue: true, defaultValue: true,
description: 'Whether to make the cursor blink in the editor', description: 'Whether to make the cursor blink in the editor',
validate: (v) => typeof v === 'boolean', validate: (v) => typeof v === 'boolean',
@ -466,11 +419,60 @@ export function createSettings() {
/** /**
* Settings that affect the behavior of project management. * Settings that affect the behavior of project management.
*/ */
projects: { project: {
directory: new Setting<string>({
defaultValue: '',
description: 'The directory to save and load projects from',
hideOnLevel: 'project',
hideOnPlatform: 'web',
validate: (v) =>
typeof v === 'string' && (v.length > 0 || !isDesktop()),
Component: ({ value, updateValue }) => {
const inputRef = useRef<HTMLInputElement>(null)
return (
<div className="flex gap-4 p-1 border rounded-sm border-chalkboard-30">
<input
className="flex-grow text-xs px-2 bg-transparent"
value={value}
disabled
data-testid="project-directory-input"
ref={inputRef}
/>
<button
onClick={toSync(async () => {
// In desktop end-to-end tests we can't control the file picker,
// so we seed the new directory value in the element's dataset
const inputRefVal = inputRef.current?.dataset.testValue
if (
inputRef.current &&
inputRefVal &&
!Array.isArray(inputRefVal)
) {
updateValue(inputRefVal)
} else {
const newPath = await window.electron.open({
properties: ['openDirectory', 'createDirectory'],
defaultPath: value,
title: 'Choose a new project directory',
})
if (newPath.canceled) return
updateValue(newPath.filePaths[0])
}
}, reportRejection)}
className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
data-testid="project-directory-button"
>
<CustomIcon name="folder" className="w-5 h-5" />
<Tooltip position="top-right">Choose a folder</Tooltip>
</button>
</div>
)
},
}),
/** /**
* The default project name to use when creating a new project * The default project name to use when creating a new project
*/ */
defaultProjectName: new Setting<string>({ default_project_name: new Setting<string>({
defaultValue: DEFAULT_PROJECT_NAME, defaultValue: DEFAULT_PROJECT_NAME,
description: description:
'The default project name to use when creating a new project', 'The default project name to use when creating a new project',
@ -504,11 +506,11 @@ export function createSettings() {
/** /**
* Settings that affect the behavior of the command bar. * Settings that affect the behavior of the command bar.
*/ */
commandBar: { command_bar: {
/** /**
* Whether to include settings in the command bar * Whether to include settings in the command bar
*/ */
includeSettings: new Setting<boolean>({ include_settings: new Setting<boolean>({
defaultValue: true, defaultValue: true,
description: 'Whether to include settings in the command bar', description: 'Whether to include settings in the command bar',
validate: (v) => typeof v === 'boolean', validate: (v) => typeof v === 'boolean',

View File

@ -26,8 +26,8 @@ describe(`testing settings initialization`, () => {
setSettingsAtLevel(settings, 'user', appSettingsPayload) setSettingsAtLevel(settings, 'user', appSettingsPayload)
expect(settings.app.theme.current).toBe('dark') expect(settings.app.appearance.theme.current).toBe('dark')
expect(settings.app.themeColor.current).toBe('190') expect(settings.app.appearance.color.current).toBe('190')
}) })
it(`doesn't read theme from project settings`, () => { it(`doesn't read theme from project settings`, () => {
@ -61,9 +61,9 @@ describe(`testing settings initialization`, () => {
setSettingsAtLevel(settings, 'project', projectSettingsPayload) setSettingsAtLevel(settings, 'project', projectSettingsPayload)
// The 'project'-level for `theme` setting should be ignored completely // The 'project'-level for `theme` setting should be ignored completely
expect(settings.app.theme.current).toBe('dark') expect(settings.app.appearance.theme.current).toBe('dark')
// But the 'project'-level for `themeColor` setting should be applied // But the 'project'-level for `themeColor` setting should be applied
expect(settings.app.themeColor.current).toBe('200') expect(settings.app.appearance.color.current).toBe('200')
}) })
}) })
@ -106,8 +106,8 @@ describe(`testing getAllCurrentSettings`, () => {
const allCurrentSettings = getAllCurrentSettings(settings) const allCurrentSettings = getAllCurrentSettings(settings)
// This one gets the 'user'-level theme because it's ignored at the project level // This one gets the 'user'-level theme because it's ignored at the project level
// (see the test "doesn't read theme from project settings") // (see the test "doesn't read theme from project settings")
expect(allCurrentSettings.app.theme).toBe('dark') expect(allCurrentSettings.app.appearance?.theme).toBe('dark')
expect(allCurrentSettings.app.themeColor).toBe('200') expect(allCurrentSettings.app.appearance?.color).toBe('200')
expect(allCurrentSettings.modeling.defaultUnit).toBe('ft') expect(allCurrentSettings.modeling.base_unit).toBe('ft')
}) })
}) })

View File

@ -34,36 +34,38 @@ export function configurationToSettingsPayload(
): DeepPartial<SaveSettingsPayload> { ): DeepPartial<SaveSettingsPayload> {
return { return {
app: { app: {
appearance: {
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme), theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
themeColor: configuration?.settings?.app?.appearance?.color color: configuration?.settings?.app?.appearance?.color
? configuration?.settings?.app?.appearance?.color.toString() ? configuration?.settings?.app?.appearance?.color.toString()
: undefined, : undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status, },
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner, onboarding_status: configuration?.settings?.app?.onboarding_status,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode, dismiss_web_banner: configuration?.settings?.app?.dismiss_web_banner,
projectDirectory: configuration?.settings?.project?.directory, stream_idle_mode: configuration?.settings?.app?.stream_idle_mode,
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
}, },
modeling: { modeling: {
defaultUnit: configuration?.settings?.modeling?.base_unit, base_unit: configuration?.settings?.modeling?.base_unit,
cameraProjection: configuration?.settings?.modeling?.camera_projection, camera_projection: configuration?.settings?.modeling?.camera_projection,
mouseControls: mouseControlsToCameraSystem( mouse_controls: mouseControlsToCameraSystem(
configuration?.settings?.modeling?.mouse_controls configuration?.settings?.modeling?.mouse_controls
), ),
highlightEdges: configuration?.settings?.modeling?.highlight_edges, highlight_edges: configuration?.settings?.modeling?.highlight_edges,
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel, show_debug_panel: configuration?.settings?.modeling?.show_debug_panel,
showScaleGrid: configuration?.settings?.modeling?.show_scale_grid, show_scale_grid: configuration?.settings?.modeling?.show_scale_grid,
enable_ssao: configuration?.settings?.modeling?.enable_ssao,
}, },
textEditor: { text_editor: {
textWrapping: configuration?.settings?.text_editor?.text_wrapping, text_wrapping: configuration?.settings?.text_editor?.text_wrapping,
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor, blinking_cursor: configuration?.settings?.text_editor?.blinking_cursor,
}, },
projects: { project: {
defaultProjectName: default_project_name:
configuration?.settings?.project?.default_project_name, configuration?.settings?.project?.default_project_name,
directory: configuration?.settings?.project?.directory,
}, },
commandBar: { command_bar: {
includeSettings: configuration?.settings?.command_bar?.include_settings, include_settings: configuration?.settings?.command_bar?.include_settings,
}, },
} }
} }
@ -73,29 +75,31 @@ export function projectConfigurationToSettingsPayload(
): DeepPartial<SaveSettingsPayload> { ): DeepPartial<SaveSettingsPayload> {
return { return {
app: { app: {
appearance: {
// do not read in `theme`, because it is blocked on the project level // do not read in `theme`, because it is blocked on the project level
themeColor: configuration?.settings?.app?.appearance?.color color: configuration?.settings?.app?.appearance?.color
? configuration?.settings?.app?.appearance?.color.toString() ? configuration?.settings?.app?.appearance?.color.toString()
: undefined, : undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status, },
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner, onboarding_status: configuration?.settings?.app?.onboarding_status,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode, dismiss_web_banner: configuration?.settings?.app?.dismiss_web_banner,
enableSSAO: configuration?.settings?.modeling?.enable_ssao, stream_idle_mode: configuration?.settings?.app?.stream_idle_mode,
}, },
modeling: { modeling: {
defaultUnit: configuration?.settings?.modeling?.base_unit, base_unit: configuration?.settings?.modeling?.base_unit,
mouseControls: mouseControlsToCameraSystem( mouse_controls: mouseControlsToCameraSystem(
configuration?.settings?.modeling?.mouse_controls configuration?.settings?.modeling?.mouse_controls
), ),
highlightEdges: configuration?.settings?.modeling?.highlight_edges, highlight_edges: configuration?.settings?.modeling?.highlight_edges,
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel, show_debug_panel: configuration?.settings?.modeling?.show_debug_panel,
enable_ssao: configuration?.settings?.modeling?.enable_ssao,
}, },
textEditor: { text_editor: {
textWrapping: configuration?.settings?.text_editor?.text_wrapping, text_wrapping: configuration?.settings?.text_editor?.text_wrapping,
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor, blinking_cursor: configuration?.settings?.text_editor?.blinking_cursor,
}, },
commandBar: { command_bar: {
includeSettings: configuration?.settings?.command_bar?.include_settings, include_settings: configuration?.settings?.command_bar?.include_settings,
}, },
} }
} }
@ -181,7 +185,7 @@ export async function loadAndValidateSettings(
// Because getting the default directory is async, we need to set it after // Because getting the default directory is async, we need to set it after
if (onDesktop) { if (onDesktop) {
settings.app.projectDirectory.default = await getInitialDefaultDir() settings.project.directory.default = await getInitialDefaultDir()
} }
settingsNext = setSettingsAtLevel( settingsNext = setSettingsAtLevel(

View File

@ -84,13 +84,13 @@ export const settingsMachine = setup({
return newContext return newContext
}), }),
setThemeClass: ({ context }) => { setThemeClass: ({ context }) => {
const currentTheme = context.app.theme.current ?? Themes.System const currentTheme = context.app.appearance.theme.current ?? Themes.System
setThemeClass( setThemeClass(
currentTheme === Themes.System ? getSystemTheme() : currentTheme currentTheme === Themes.System ? getSystemTheme() : currentTheme
) )
}, },
setEngineCameraProjection: ({ context }) => { setEngineCameraProjection: ({ context }) => {
const newCurrentProjection = context.modeling.cameraProjection.current const newCurrentProjection = context.modeling.camera_projection.current
sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection) sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection)
}, },
}, },

View File

@ -79,13 +79,13 @@ const Home = () => {
send({ send({
type: 'assign', type: 'assign',
data: { data: {
defaultProjectName: settings.projects.defaultProjectName.current, defaultProjectName: settings.project.default_project_name.current,
defaultDirectory: settings.app.projectDirectory.current, defaultDirectory: settings.project.directory.current,
}, },
}) })
}, [ }, [
settings.app.projectDirectory.current, settings.project.directory.current,
settings.projects.defaultProjectName.current, settings.project.default_project_name.current,
send, send,
]) ])
@ -134,7 +134,7 @@ const Home = () => {
groupId: 'projects', groupId: 'projects',
name: 'Create project', name: 'Create project',
argDefaultValues: { argDefaultValues: {
name: settings.projects.defaultProjectName.current, name: settings.project.default_project_name.current,
}, },
}, },
}) })
@ -207,7 +207,7 @@ const Home = () => {
to={`${PATHS.HOME + PATHS.SETTINGS_USER}#projectDirectory`} to={`${PATHS.HOME + PATHS.SETTINGS_USER}#projectDirectory`}
className="text-chalkboard-90 dark:text-chalkboard-20 underline underline-offset-2" className="text-chalkboard-90 dark:text-chalkboard-20 underline underline-offset-2"
> >
{settings.app.projectDirectory.current} {settings.project.directory.current}
</Link> </Link>
. .
</p> </p>

View File

@ -16,7 +16,7 @@ export default function Units() {
send, send,
state: { state: {
context: { context: {
modeling: { mouseControls }, modeling: { mouse_controls },
}, },
}, },
}, },
@ -38,10 +38,10 @@ export default function Units() {
<select <select
id="camera-controls" id="camera-controls"
className="block w-full px-3 py-1 bg-transparent border border-chalkboard-30" className="block w-full px-3 py-1 bg-transparent border border-chalkboard-30"
value={mouseControls.current} value={mouse_controls.current}
onChange={(e) => { onChange={(e) => {
send({ send({
type: 'set.modeling.mouseControls', type: 'set.modeling.mouse_controls',
data: { data: {
level: 'user', level: 'user',
value: e.target.value as CameraSystem, value: e.target.value as CameraSystem,
@ -58,15 +58,15 @@ export default function Units() {
<ul className="mx-4 my-2 text-sm leading-relaxed"> <ul className="mx-4 my-2 text-sm leading-relaxed">
<li> <li>
<strong>Pan:</strong>{' '} <strong>Pan:</strong>{' '}
{cameraMouseDragGuards[mouseControls.current].pan.description} {cameraMouseDragGuards[mouse_controls.current].pan.description}
</li> </li>
<li> <li>
<strong>Zoom:</strong>{' '} <strong>Zoom:</strong>{' '}
{cameraMouseDragGuards[mouseControls.current].zoom.description} {cameraMouseDragGuards[mouse_controls.current].zoom.description}
</li> </li>
<li> <li>
<strong>Rotate:</strong>{' '} <strong>Rotate:</strong>{' '}
{cameraMouseDragGuards[mouseControls.current].rotate.description} {cameraMouseDragGuards[mouse_controls.current].rotate.description}
</li> </li>
</ul> </ul>
</SettingsSection> </SettingsSection>

View File

@ -126,7 +126,9 @@ function OnboardingIntroductionInner() {
settings: { settings: {
state: { state: {
context: { context: {
app: { theme }, app: {
appearance: { theme },
},
}, },
}, },
}, },

View File

@ -11,10 +11,12 @@ export default function OnboardingParametricModeling() {
settings: { settings: {
context: { context: {
app: { app: {
appearance: {
theme: { current: theme }, theme: { current: theme },
}, },
}, },
}, },
},
} = useSettingsAuthContext() } = useSettingsAuthContext()
const getImageTheme = () => const getImageTheme = () =>
theme === Themes.Light || theme === Themes.Light ||

View File

@ -13,7 +13,7 @@ export default function Units() {
settings: { settings: {
send, send,
context: { context: {
modeling: { defaultUnit }, modeling: { base_unit },
}, },
}, },
} = useSettingsAuthContext() } = useSettingsAuthContext()
@ -29,10 +29,10 @@ export default function Units() {
<select <select
id="base-unit" id="base-unit"
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent" className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
value={defaultUnit.user} value={base_unit.user}
onChange={(e) => { onChange={(e) => {
send({ send({
type: 'set.modeling.defaultUnit', type: 'set.modeling.base_unit',
data: { data: {
level: 'user', level: 'user',
value: e.target.value as BaseUnit, value: e.target.value as BaseUnit,

View File

@ -116,7 +116,7 @@ export function useNextClick(newStatus: string) {
return useCallback(() => { return useCallback(() => {
send({ send({
type: 'set.app.onboardingStatus', type: 'set.app.onboarding_status',
data: { level: 'user', value: newStatus }, data: { level: 'user', value: newStatus },
}) })
navigate(filePath + PATHS.ONBOARDING.INDEX.slice(0, -1) + newStatus) navigate(filePath + PATHS.ONBOARDING.INDEX.slice(0, -1) + newStatus)
@ -132,7 +132,7 @@ export function useDismiss() {
const settingsCallback = useCallback(() => { const settingsCallback = useCallback(() => {
send({ send({
type: 'set.app.onboardingStatus', type: 'set.app.onboarding_status',
data: { level: 'user', value: 'dismissed' }, data: { level: 'user', value: 'dismissed' },
}) })
}, [send]) }, [send])
@ -143,7 +143,7 @@ export function useDismiss() {
*/ */
useEffect(() => { useEffect(() => {
if ( if (
state.context.app.onboardingStatus.user === 'dismissed' && state.context.app.onboarding_status.user === 'dismissed' &&
state.matches('idle') state.matches('idle')
) { ) {
navigate(filePath) navigate(filePath)

View File

@ -26,7 +26,9 @@ const SignIn = () => {
settings: { settings: {
state: { state: {
context: { context: {
app: { theme }, app: {
appearance: { theme },
},
}, },
}, },
}, },