Add a user-level projection setting, command, and toggle (#3983)

* Add cameraProjection setting

* Add UI to toggle the user-level projection setting.

* Make cameraProjection setting respected at startup

* Add an E2E test for the perspective toggle

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Don't force user back into perspective when exiting sketch

* Make the projection setting more searchable

* Make `current` label apply to the default option if not set

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* Re-run CI

* Ohh *cargo fmt*

* @lf94 feedback, fix found toggling bug, make command bar instantly toggle setting

* Roll back the instant toggling behavior, it breaks the tests

* Make ortho the default, keep tests using perspective

* Move projection below camera controls setting

* Fix up gizmo tests, which broke because the gizmo moved

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)

* Look at this (photo)Graph *in the voice of Nickelback*

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: 49fl <ircsurfer33@gmail.com>
This commit is contained in:
Frank Noirot
2024-09-30 11:40:00 -04:00
committed by GitHub
parent 3949b6acf4
commit 2e72f235dd
55 changed files with 315 additions and 20 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -15,6 +15,7 @@ export const TEST_SETTINGS = {
modeling: {
defaultUnit: 'in',
mouseControls: 'KittyCAD',
cameraProjection: 'perspective',
showDebugPanel: true,
},
projects: {
@ -62,6 +63,7 @@ export const TEST_SETTINGS_CORRUPTED = {
modeling: {
defaultUnit: 'invalid' as any,
mouseControls: `() => alert('hack the planet')` as any,
cameraProjection: 'perspective',
showDebugPanel: true,
},
projects: {

View File

@ -16,37 +16,37 @@ test.describe('Testing Gizmo', () => {
const cases = [
{
testDescription: 'top view',
clickPosition: { x: 951, y: 385 },
clickPosition: { x: 951, y: 347 },
expectedCameraPosition: { x: 800, y: -152, z: 4886.02 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'bottom view',
clickPosition: { x: 951, y: 429 },
clickPosition: { x: 951, y: 391 },
expectedCameraPosition: { x: 800, y: -152, z: -4834.02 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'right view',
clickPosition: { x: 929, y: 417 },
clickPosition: { x: 929, y: 379 },
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'left view',
clickPosition: { x: 974, y: 397 },
clickPosition: { x: 974, y: 359 },
expectedCameraPosition: { x: -4060.02, y: -152, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'back view',
clickPosition: { x: 967, y: 421 },
clickPosition: { x: 967, y: 383 },
expectedCameraPosition: { x: 800, y: 4708.02, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},
{
testDescription: 'front view',
clickPosition: { x: 935, y: 393 },
clickPosition: { x: 935, y: 355 },
expectedCameraPosition: { x: 800, y: -5012.02, z: 26 },
expectedCameraTarget: { x: 800, y: -152, z: 26 },
},

View File

@ -0,0 +1,112 @@
import { test, expect } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates'
import * as TOML from '@iarna/toml'
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('Test toggling perspective', () => {
test('via command palette and toggle', async ({ page }) => {
const u = await getUtils(page)
// Locators and constants
const screenWidth = 1200
const screenHeight = 500
const checkedScreenLocation = {
x: screenWidth * 0.71,
y: screenHeight * 0.4,
}
const backgroundColor: [number, number, number] = [29, 29, 29]
const xzPlaneColor: [number, number, number] = [50, 50, 99]
const locationToHaveColor = async (color: [number, number, number]) => {
return u.getGreatestPixDiff(checkedScreenLocation, color)
}
const commandPaletteButton = page.getByRole('button', { name: 'Commands' })
const commandOption = page.getByRole('option', {
name: 'camera projection',
})
const orthoOption = page.getByRole('option', { name: 'orthographic' })
const commandToast = page.getByText(
`Set camera projection to "orthographic"`
)
const projectionToggle = page.getByRole('switch', {
name: 'Camera projection: ',
})
await test.step('Setup', async () => {
await page.setViewportSize({ width: screenWidth, height: screenHeight })
await u.waitForAuthSkipAppStart()
await u.closeKclCodePanel()
await expect
.poll(async () => locationToHaveColor(backgroundColor), {
timeout: 5000,
message: 'This spot should have the background color',
})
.toBeLessThan(15)
await expect(projectionToggle).toHaveAttribute('aria-checked', 'true')
})
await test.step('Switch to ortho via command palette', async () => {
await commandPaletteButton.click()
await commandOption.click()
await orthoOption.click()
await expect(commandToast).toBeVisible()
await expect
.poll(async () => locationToHaveColor(xzPlaneColor), {
timeout: 5000,
message: 'This spot should have the XZ plane color',
})
.toBeLessThan(15)
await expect(projectionToggle).toHaveAttribute('aria-checked', 'false')
})
await test.step(`Refresh the page and ensure the stream is loaded in ortho`, async () => {
// In playwright web, the settings set while testing are not persisted because
// the `addInitScript` within `setup` is re-run on page reload
await page.addInitScript(
({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: TOML.stringify({
settings: {
...TEST_SETTINGS,
modeling: {
...TEST_SETTINGS.modeling,
cameraProjection: 'orthographic',
},
},
}),
}
)
await page.reload()
await u.waitForAuthSkipAppStart()
await expect
.poll(async () => locationToHaveColor(xzPlaneColor), {
timeout: 5000,
message: 'This spot should have the XZ plane color',
})
.toBeLessThan(15)
await expect(commandToast).not.toBeVisible()
await expect(projectionToggle).toHaveAttribute('aria-checked', 'false')
})
await test.step(`Switch to perspective via toggle`, async () => {
await projectionToggle.click()
await expect(projectionToggle).toHaveAttribute('aria-checked', 'true')
await expect
.poll(async () => locationToHaveColor(backgroundColor), {
timeout: 5000,
message: 'This spot should have the background color',
})
.toBeLessThan(15)
})
})
})

View File

@ -21,6 +21,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
import Gizmo from 'components/Gizmo'
import { CoreDumpManager } from 'lib/coredump'
import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
export function App() {
const { project, file } = useLoaderData() as IndexLoaderData
@ -85,6 +86,7 @@ export function App() {
<LowerRightControls coreDumpManager={coreDumpManager}>
<UnitsMenu />
<Gizmo />
<CameraProjectionToggle />
</LowerRightControls>
</div>
)

View File

@ -28,6 +28,7 @@ import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js'
import { isQuaternionVertical } from './helpers'
import { reportRejection } from 'lib/trap'
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
const ORTHOGRAPHIC_CAMERA_SIZE = 20
const FRAMES_TO_ANIMATE_IN = 30
@ -90,6 +91,14 @@ export class CameraControls {
return this.camera instanceof PerspectiveCamera
}
setEngineCameraProjection(projection: CameraProjectionType) {
if (projection === 'orthographic') {
this.useOrthographicCamera()
} else {
this.usePerspectiveCamera(true).catch(reportRejection)
}
}
handleStart = () => {
this._isCamMovingCallback(true, false)
}

View File

@ -43,6 +43,7 @@ import {
} from 'lang/modifyAst'
import { ActionButton } from 'components/ActionButton'
import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false)
@ -718,6 +719,7 @@ export const CamDebugSettings = () => {
sceneInfra.camControls.reactCameraProperties
)
const [fov, setFov] = useState(12)
const { commandBarSend } = useCommandsContext()
useEffect(() => {
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
@ -735,14 +737,15 @@ export const CamDebugSettings = () => {
<input
type="checkbox"
checked={camSettings.type === 'perspective'}
onChange={(e) => {
if (camSettings.type === 'perspective') {
sceneInfra.camControls.useOrthographicCamera()
} else {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneInfra.camControls.usePerspectiveCamera(true)
onChange={() =>
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'settings',
name: 'modeling.cameraProjection',
},
})
}
}}
/>
<div>
<button

View File

@ -0,0 +1,59 @@
import { Switch } from '@headlessui/react'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useEffect, useState } from 'react'
export function CameraProjectionToggle() {
const { settings } = useSettingsAuthContext()
const isCameraProjectionPerspective =
settings.context.modeling.cameraProjection.current === 'perspective'
const [checked, setChecked] = useState(isCameraProjectionPerspective)
useEffect(() => {
setChecked(
settings.context.modeling.cameraProjection.current === 'perspective'
)
}, [settings.context.modeling.cameraProjection.current])
return (
<Switch
checked={checked}
onChange={(newValue) => {
settings.send({
type: 'set.modeling.cameraProjection',
data: {
level: 'user',
value: newValue ? 'perspective' : 'orthographic',
},
})
}}
className={`pointer-events-auto p-0 text-xs text-chalkboard-60 dark:text-chalkboard-40 bg-chalkboard-10/70 hover:bg-chalkboard-10 dark:bg-chalkboard-100/80 dark:hover:bg-chalkboard-100 backdrop-blur-sm
border border-primary/10 hover:border-primary/50 focus-visible:border-primary/50 rounded-full`}
>
<span className="sr-only">Camera projection: </span>
<div className="flex items-center gap-2">
<span
aria-hidden={checked}
className={
'border border-solid m-[-1px] rounded-full px-2 py-1 ' +
(!checked
? 'text-primary border-primary -mr-2'
: 'border-transparent')
}
>
Orthographic
</span>
<span
aria-hidden={checked}
className={
'border border-solid m-[-1px] rounded-full px-2 py-1 ' +
(checked
? 'text-primary border-primary -ml-2'
: 'border-transparent')
}
>
Perspective
</span>
</div>
</Switch>
)
}

View File

@ -22,6 +22,7 @@ function CommandComboBox({
const fuse = new Fuse(options, {
keys: ['displayName', 'name', 'description'],
threshold: 0.3,
ignoreLocation: true,
})
useEffect(() => {

View File

@ -104,7 +104,12 @@ export const ModelingMachineProvider = ({
settings: {
context: {
app: { theme, enableSSAO },
modeling: { defaultUnit, highlightEdges, showScaleGrid },
modeling: {
defaultUnit,
cameraProjection,
highlightEdges,
showScaleGrid,
},
},
},
} = useSettingsAuthContext()
@ -145,7 +150,9 @@ export const ModelingMachineProvider = ({
;(async () => {
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
@ -974,6 +981,7 @@ export const ModelingMachineProvider = ({
highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current,
showScaleGrid: showScaleGrid.current,
cameraProjection: cameraProjection.current,
},
token
)

View File

@ -22,6 +22,7 @@ export function useSetupEngineManager(
highlightEdges: true,
enableSSAO: true,
showScaleGrid: false,
cameraProjection: 'perspective',
} as SettingsViaQueryString,
token?: string
) {

View File

@ -1380,6 +1380,7 @@ export class EngineCommandManager extends EventTarget {
highlightEdges: true,
enableSSAO: true,
showScaleGrid: false,
cameraProjection: 'perspective',
}
}
@ -1431,6 +1432,7 @@ export class EngineCommandManager extends EventTarget {
highlightEdges: true,
enableSSAO: true,
showScaleGrid: false,
cameraProjection: 'orthographic',
},
// When passed, use a completely separate connecting code path that simply
// opens a websocket and this is a function that is called when connected.
@ -1487,6 +1489,19 @@ export class EngineCommandManager extends EventTarget {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.onEngineConnectionOpened = async () => {
// Set the stream's camera projection type
// We don't send a command to the engine if in perspective mode because
// for now it's the engine's default.
if (settings.cameraProjection === 'orthographic') {
this.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_orthographic',
},
}).catch(reportRejection)
}
// Set the theme
this.setTheme(this.settings.theme).catch(reportRejection)
// Set up a listener for the dark theme media query

View File

@ -18,6 +18,7 @@ import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
/**
* A setting that can be set at the user or project level
@ -100,6 +101,18 @@ export class Setting<T = unknown> {
: this._default
: this._default
}
/**
* For the purposes of showing the `current` label in the command bar,
* is this setting at the given level the same as the given value?
*/
public shouldShowCurrentLabel(
level: SettingsLevel | 'default',
valueToMatch: T
): boolean {
return this[`_${level}`] === undefined
? this.getFallback(level) === valueToMatch
: this[`_${level}`] === valueToMatch
}
public getParentLevel(level: SettingsLevel): SettingsLevel | 'default' {
return level === 'project' ? 'user' : 'default'
}
@ -284,9 +297,9 @@ export function createSettings() {
value: v,
isCurrent:
v ===
settingsContext.modeling.mouseControls[
settingsContext.modeling.mouseControls.shouldShowCurrentLabel(
cmdContext.argumentsToSubmit.level as SettingsLevel
],
),
})),
},
Component: ({ value, updateValue }) => (
@ -326,6 +339,36 @@ export function createSettings() {
</>
),
}),
/**
* Projection method applied to the 3D view, perspective or orthographic
*/
cameraProjection: new Setting<CameraProjectionType>({
defaultValue: 'orthographic',
hideOnLevel: 'project',
description:
'Projection method applied to the 3D view, perspective or orthographic',
validate: (v) => ['perspective', 'orthographic'].includes(v),
commandConfig: {
inputType: 'options',
// This is how we could have toggling behavior for a non-boolean argument:
// Set it to "skippable", and make the default value the opposite of the current value
// skip: true,
defaultValueFromContext: (context) =>
context.modeling.cameraProjection.current === 'perspective'
? 'orthographic'
: 'perspective',
options: (cmdContext, settingsContext) =>
(['perspective', 'orthographic'] as const).map((v) => ({
name: v.charAt(0).toUpperCase() + v.slice(1),
value: v,
isCurrent:
settingsContext.modeling.cameraProjection.shouldShowCurrentLabel(
cmdContext.argumentsToSubmit.level as SettingsLevel,
v
),
})),
},
}),
/**
* Whether to highlight edges of 3D objects
*/

View File

@ -3,6 +3,7 @@ import { Setting, settings } from './initialSettings'
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'
export interface SettingsViaQueryString {
pool: string | null
@ -10,6 +11,7 @@ export interface SettingsViaQueryString {
highlightEdges: boolean
enableSSAO: boolean
showScaleGrid: boolean
cameraProjection: CameraProjectionType
}
export enum UnitSystem {

View File

@ -46,6 +46,7 @@ export function configurationToSettingsPayload(
},
modeling: {
defaultUnit: configuration?.settings?.modeling?.base_unit,
cameraProjection: configuration?.settings?.modeling?.camera_projection,
mouseControls: mouseControlsToCameraSystem(
configuration?.settings?.modeling?.mouse_controls
),

View File

@ -13,6 +13,7 @@ import {
projectConfigurationToSettingsPayload,
setSettingsAtLevel,
} from 'lib/settings/settingsUtils'
import { sceneInfra } from 'lib/singletons'
export const settingsMachine = setup({
types: {
@ -89,6 +90,10 @@ export const settingsMachine = setup({
currentTheme === Themes.System ? getSystemTheme() : currentTheme
)
},
setEngineCameraProjection: ({ context }) => {
const newCurrentProjection = context.modeling.cameraProjection.current
sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection)
},
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
@ -153,6 +158,16 @@ export const settingsMachine = setup({
actions: ['setSettingAtLevel', 'toastSuccess'],
},
'set.modeling.cameraProjection': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
'setEngineCameraProjection',
],
},
'set.modeling.highlightEdges': {
target: 'persisting settings',

View File

@ -254,6 +254,9 @@ pub struct ModelingSettings {
/// The default unit to use in modeling dimensions.
#[serde(default, alias = "defaultUnit", skip_serializing_if = "is_default")]
pub base_unit: UnitLength,
/// The projection mode the camera should use while modeling.
#[serde(default, alias = "cameraProjection", skip_serializing_if = "is_default")]
pub camera_projection: CameraProjectionType,
/// The controls for how to navigate the 3D view.
#[serde(default, alias = "mouseControls", skip_serializing_if = "is_default")]
pub mouse_controls: MouseControlType,
@ -397,6 +400,19 @@ pub enum MouseControlType {
AutoCad,
}
/// The types of camera projection for the 3D view.
#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum CameraProjectionType {
/// Perspective projection https://en.wikipedia.org/wiki/3D_projection#Perspective_projection
Perspective,
/// Orthographic projection https://en.wikipedia.org/wiki/3D_projection#Orthographic_projection
#[default]
Orthographic,
}
/// Settings that affect the behavior of the KCL text editor.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
#[serde(rename_all = "snake_case")]
@ -522,8 +538,8 @@ mod tests {
use validator::Validate;
use super::{
AppColor, AppSettings, AppTheme, AppearanceSettings, CommandBarSettings, Configuration, ModelingSettings,
OnboardingStatus, ProjectSettings, Settings, TextEditorSettings, UnitLength,
AppColor, AppSettings, AppTheme, AppearanceSettings, CameraProjectionType, CommandBarSettings, Configuration,
ModelingSettings, OnboardingStatus, ProjectSettings, Settings, TextEditorSettings, UnitLength,
};
#[test]
@ -538,6 +554,7 @@ enableSSAO = false
[settings.modeling]
defaultUnit = "in"
cameraProjection = "orthographic"
mouseControls = "KittyCAD"
showDebugPanel = true
@ -569,6 +586,7 @@ textWrapping = true
},
modeling: ModelingSettings {
base_unit: UnitLength::In,
camera_projection: CameraProjectionType::Orthographic,
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
@ -629,6 +647,7 @@ includeSettings = false
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
camera_projection: Default::default(),
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
@ -694,6 +713,7 @@ defaultProjectName = "projects-$nnn"
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
camera_projection: Default::default(),
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
@ -771,6 +791,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
},
modeling: ModelingSettings {
base_unit: UnitLength::Mm,
camera_projection: Default::default(),
mouse_controls: Default::default(),
highlight_edges: true.into(),
show_debug_panel: false,

View File

@ -127,6 +127,7 @@ includeSettings = false
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
camera_projection: Default::default(),
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,