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>
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 36 KiB |
@ -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: {
|
||||
|
@ -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 },
|
||||
},
|
||||
|
112
e2e/playwright/testing-perspective-toggle.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
@ -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>
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
59
src/components/CameraProjectionToggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -22,6 +22,7 @@ function CommandComboBox({
|
||||
const fuse = new Fuse(options, {
|
||||
keys: ['displayName', 'name', 'description'],
|
||||
threshold: 0.3,
|
||||
ignoreLocation: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -22,6 +22,7 @@ export function useSetupEngineManager(
|
||||
highlightEdges: true,
|
||||
enableSSAO: true,
|
||||
showScaleGrid: false,
|
||||
cameraProjection: 'perspective',
|
||||
} as SettingsViaQueryString,
|
||||
token?: string
|
||||
) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
),
|
||||
|
@ -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',
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|