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: {
|
modeling: {
|
||||||
defaultUnit: 'in',
|
defaultUnit: 'in',
|
||||||
mouseControls: 'KittyCAD',
|
mouseControls: 'KittyCAD',
|
||||||
|
cameraProjection: 'perspective',
|
||||||
showDebugPanel: true,
|
showDebugPanel: true,
|
||||||
},
|
},
|
||||||
projects: {
|
projects: {
|
||||||
@ -62,6 +63,7 @@ export const TEST_SETTINGS_CORRUPTED = {
|
|||||||
modeling: {
|
modeling: {
|
||||||
defaultUnit: 'invalid' as any,
|
defaultUnit: 'invalid' as any,
|
||||||
mouseControls: `() => alert('hack the planet')` as any,
|
mouseControls: `() => alert('hack the planet')` as any,
|
||||||
|
cameraProjection: 'perspective',
|
||||||
showDebugPanel: true,
|
showDebugPanel: true,
|
||||||
},
|
},
|
||||||
projects: {
|
projects: {
|
||||||
|
@ -16,37 +16,37 @@ test.describe('Testing Gizmo', () => {
|
|||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
testDescription: 'top view',
|
testDescription: 'top view',
|
||||||
clickPosition: { x: 951, y: 385 },
|
clickPosition: { x: 951, y: 347 },
|
||||||
expectedCameraPosition: { x: 800, y: -152, z: 4886.02 },
|
expectedCameraPosition: { x: 800, y: -152, z: 4886.02 },
|
||||||
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testDescription: 'bottom view',
|
testDescription: 'bottom view',
|
||||||
clickPosition: { x: 951, y: 429 },
|
clickPosition: { x: 951, y: 391 },
|
||||||
expectedCameraPosition: { x: 800, y: -152, z: -4834.02 },
|
expectedCameraPosition: { x: 800, y: -152, z: -4834.02 },
|
||||||
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testDescription: 'right view',
|
testDescription: 'right view',
|
||||||
clickPosition: { x: 929, y: 417 },
|
clickPosition: { x: 929, y: 379 },
|
||||||
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
|
expectedCameraPosition: { x: 5660.02, y: -152, z: 26 },
|
||||||
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testDescription: 'left view',
|
testDescription: 'left view',
|
||||||
clickPosition: { x: 974, y: 397 },
|
clickPosition: { x: 974, y: 359 },
|
||||||
expectedCameraPosition: { x: -4060.02, y: -152, z: 26 },
|
expectedCameraPosition: { x: -4060.02, y: -152, z: 26 },
|
||||||
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testDescription: 'back view',
|
testDescription: 'back view',
|
||||||
clickPosition: { x: 967, y: 421 },
|
clickPosition: { x: 967, y: 383 },
|
||||||
expectedCameraPosition: { x: 800, y: 4708.02, z: 26 },
|
expectedCameraPosition: { x: 800, y: 4708.02, z: 26 },
|
||||||
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
expectedCameraTarget: { x: 800, y: -152, z: 26 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testDescription: 'front view',
|
testDescription: 'front view',
|
||||||
clickPosition: { x: 935, y: 393 },
|
clickPosition: { x: 935, y: 355 },
|
||||||
expectedCameraPosition: { x: 800, y: -5012.02, z: 26 },
|
expectedCameraPosition: { x: 800, y: -5012.02, z: 26 },
|
||||||
expectedCameraTarget: { x: 800, y: -152, 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 Gizmo from 'components/Gizmo'
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { UnitsMenu } from 'components/UnitsMenu'
|
import { UnitsMenu } from 'components/UnitsMenu'
|
||||||
|
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
const { project, file } = useLoaderData() as IndexLoaderData
|
||||||
@ -85,6 +86,7 @@ export function App() {
|
|||||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||||
<UnitsMenu />
|
<UnitsMenu />
|
||||||
<Gizmo />
|
<Gizmo />
|
||||||
|
<CameraProjectionToggle />
|
||||||
</LowerRightControls>
|
</LowerRightControls>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -28,6 +28,7 @@ import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
|||||||
import * as TWEEN from '@tweenjs/tween.js'
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
import { isQuaternionVertical } from './helpers'
|
import { isQuaternionVertical } from './helpers'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
|
||||||
|
|
||||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||||
const FRAMES_TO_ANIMATE_IN = 30
|
const FRAMES_TO_ANIMATE_IN = 30
|
||||||
@ -90,6 +91,14 @@ export class CameraControls {
|
|||||||
return this.camera instanceof PerspectiveCamera
|
return this.camera instanceof PerspectiveCamera
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEngineCameraProjection(projection: CameraProjectionType) {
|
||||||
|
if (projection === 'orthographic') {
|
||||||
|
this.useOrthographicCamera()
|
||||||
|
} else {
|
||||||
|
this.usePerspectiveCamera(true).catch(reportRejection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleStart = () => {
|
handleStart = () => {
|
||||||
this._isCamMovingCallback(true, false)
|
this._isCamMovingCallback(true, false)
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
|
||||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||||
@ -718,6 +719,7 @@ export const CamDebugSettings = () => {
|
|||||||
sceneInfra.camControls.reactCameraProperties
|
sceneInfra.camControls.reactCameraProperties
|
||||||
)
|
)
|
||||||
const [fov, setFov] = useState(12)
|
const [fov, setFov] = useState(12)
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
||||||
@ -735,14 +737,15 @@ export const CamDebugSettings = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={camSettings.type === 'perspective'}
|
checked={camSettings.type === 'perspective'}
|
||||||
onChange={(e) => {
|
onChange={() =>
|
||||||
if (camSettings.type === 'perspective') {
|
commandBarSend({
|
||||||
sceneInfra.camControls.useOrthographicCamera()
|
type: 'Find and select command',
|
||||||
} else {
|
data: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
groupId: 'settings',
|
||||||
sceneInfra.camControls.usePerspectiveCamera(true)
|
name: 'modeling.cameraProjection',
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<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, {
|
const fuse = new Fuse(options, {
|
||||||
keys: ['displayName', 'name', 'description'],
|
keys: ['displayName', 'name', 'description'],
|
||||||
threshold: 0.3,
|
threshold: 0.3,
|
||||||
|
ignoreLocation: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -104,7 +104,12 @@ export const ModelingMachineProvider = ({
|
|||||||
settings: {
|
settings: {
|
||||||
context: {
|
context: {
|
||||||
app: { theme, enableSSAO },
|
app: { theme, enableSSAO },
|
||||||
modeling: { defaultUnit, highlightEdges, showScaleGrid },
|
modeling: {
|
||||||
|
defaultUnit,
|
||||||
|
cameraProjection,
|
||||||
|
highlightEdges,
|
||||||
|
showScaleGrid,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
@ -145,7 +150,9 @@ export const ModelingMachineProvider = ({
|
|||||||
;(async () => {
|
;(async () => {
|
||||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||||
|
|
||||||
|
if (cameraProjection.current === 'perspective') {
|
||||||
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
|
||||||
|
}
|
||||||
|
|
||||||
sceneInfra.camControls.syncDirection = 'engineToClient'
|
sceneInfra.camControls.syncDirection = 'engineToClient'
|
||||||
|
|
||||||
@ -974,6 +981,7 @@ export const ModelingMachineProvider = ({
|
|||||||
highlightEdges: highlightEdges.current,
|
highlightEdges: highlightEdges.current,
|
||||||
enableSSAO: enableSSAO.current,
|
enableSSAO: enableSSAO.current,
|
||||||
showScaleGrid: showScaleGrid.current,
|
showScaleGrid: showScaleGrid.current,
|
||||||
|
cameraProjection: cameraProjection.current,
|
||||||
},
|
},
|
||||||
token
|
token
|
||||||
)
|
)
|
||||||
|
@ -22,6 +22,7 @@ export function useSetupEngineManager(
|
|||||||
highlightEdges: true,
|
highlightEdges: true,
|
||||||
enableSSAO: true,
|
enableSSAO: true,
|
||||||
showScaleGrid: false,
|
showScaleGrid: false,
|
||||||
|
cameraProjection: 'perspective',
|
||||||
} as SettingsViaQueryString,
|
} as SettingsViaQueryString,
|
||||||
token?: string
|
token?: string
|
||||||
) {
|
) {
|
||||||
|
@ -1380,6 +1380,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
highlightEdges: true,
|
highlightEdges: true,
|
||||||
enableSSAO: true,
|
enableSSAO: true,
|
||||||
showScaleGrid: false,
|
showScaleGrid: false,
|
||||||
|
cameraProjection: 'perspective',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1431,6 +1432,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
highlightEdges: true,
|
highlightEdges: true,
|
||||||
enableSSAO: true,
|
enableSSAO: true,
|
||||||
showScaleGrid: false,
|
showScaleGrid: false,
|
||||||
|
cameraProjection: 'orthographic',
|
||||||
},
|
},
|
||||||
// When passed, use a completely separate connecting code path that simply
|
// When passed, use a completely separate connecting code path that simply
|
||||||
// opens a websocket and this is a function that is called when connected.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.onEngineConnectionOpened = async () => {
|
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
|
// Set the theme
|
||||||
this.setTheme(this.settings.theme).catch(reportRejection)
|
this.setTheme(this.settings.theme).catch(reportRejection)
|
||||||
// Set up a listener for the dark theme media query
|
// 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 Tooltip from 'components/Tooltip'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
import { reportRejection } from 'lib/trap'
|
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
|
* 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
|
||||||
: 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' {
|
public getParentLevel(level: SettingsLevel): SettingsLevel | 'default' {
|
||||||
return level === 'project' ? 'user' : 'default'
|
return level === 'project' ? 'user' : 'default'
|
||||||
}
|
}
|
||||||
@ -284,9 +297,9 @@ export function createSettings() {
|
|||||||
value: v,
|
value: v,
|
||||||
isCurrent:
|
isCurrent:
|
||||||
v ===
|
v ===
|
||||||
settingsContext.modeling.mouseControls[
|
settingsContext.modeling.mouseControls.shouldShowCurrentLabel(
|
||||||
cmdContext.argumentsToSubmit.level as SettingsLevel
|
cmdContext.argumentsToSubmit.level as SettingsLevel
|
||||||
],
|
),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
Component: ({ value, updateValue }) => (
|
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
|
* Whether to highlight edges of 3D objects
|
||||||
*/
|
*/
|
||||||
|
@ -3,6 +3,7 @@ import { Setting, settings } from './initialSettings'
|
|||||||
import { AtLeast, PathValue, Paths } from 'lib/types'
|
import { AtLeast, PathValue, Paths } from 'lib/types'
|
||||||
import { CommandArgumentConfig } from 'lib/commandTypes'
|
import { CommandArgumentConfig } from 'lib/commandTypes'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
|
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
|
||||||
|
|
||||||
export interface SettingsViaQueryString {
|
export interface SettingsViaQueryString {
|
||||||
pool: string | null
|
pool: string | null
|
||||||
@ -10,6 +11,7 @@ export interface SettingsViaQueryString {
|
|||||||
highlightEdges: boolean
|
highlightEdges: boolean
|
||||||
enableSSAO: boolean
|
enableSSAO: boolean
|
||||||
showScaleGrid: boolean
|
showScaleGrid: boolean
|
||||||
|
cameraProjection: CameraProjectionType
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UnitSystem {
|
export enum UnitSystem {
|
||||||
|
@ -46,6 +46,7 @@ export function configurationToSettingsPayload(
|
|||||||
},
|
},
|
||||||
modeling: {
|
modeling: {
|
||||||
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
||||||
|
cameraProjection: configuration?.settings?.modeling?.camera_projection,
|
||||||
mouseControls: mouseControlsToCameraSystem(
|
mouseControls: mouseControlsToCameraSystem(
|
||||||
configuration?.settings?.modeling?.mouse_controls
|
configuration?.settings?.modeling?.mouse_controls
|
||||||
),
|
),
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
projectConfigurationToSettingsPayload,
|
projectConfigurationToSettingsPayload,
|
||||||
setSettingsAtLevel,
|
setSettingsAtLevel,
|
||||||
} from 'lib/settings/settingsUtils'
|
} from 'lib/settings/settingsUtils'
|
||||||
|
import { sceneInfra } from 'lib/singletons'
|
||||||
|
|
||||||
export const settingsMachine = setup({
|
export const settingsMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
@ -89,6 +90,10 @@ export const settingsMachine = setup({
|
|||||||
currentTheme === Themes.System ? getSystemTheme() : currentTheme
|
currentTheme === Themes.System ? getSystemTheme() : currentTheme
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
setEngineCameraProjection: ({ context }) => {
|
||||||
|
const newCurrentProjection = context.modeling.cameraProjection.current
|
||||||
|
sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwEMAHW-Ae2wCNHqAnCHKZNatAFdYAbQAMAXUShajWJizNpIAB6IALAFYAnPgBMARgDsBsQDY969QGYjmzQBoQAT0SnrADnwePY61r0PAwNtMyMAX3CnVAweAiJSCio6BjQACzAAWzAAYUZiRg5xKSQQWXlFbGU1BD1PfFtfE3UzTUNNaydXBCD1b209PTEPTTMtdQNNSOj0LFx4knJKNHxMxggwYh58DYAzakFiNABVbAVi5XKFTCVSmusxPXx7bRt1DzMxI3UjD3UutwhAz4MyeHxiV5+AYRKIgGJzPCERZJFYpfDpLJgC6lK6VaqIExPMwWGwdGxBPRmAE9PSafCPMQ-EzWbQ6ELTOGzOJIxLLVbrTbbNKYKBpLaitAAUWgcGxMjk11uoBqVmBH0ZLKCrVs-xciCCwLCvhCjyMFhGHPh3IS5AASnB0AACZYI0SSS4KvF3AlafADRl1YZ2IxiRx6hBtIzPb7abQ+DxGaxmYKWrnzHnkGKO6jEYjOtN4OVlT03KrehAtOnm7Qaup6Ixm6mR6OaR4dAwjM1mVOxdM2lH8jZbXD4WBpRgAd2QAGMc2AAOIcIhF3Gl-EIRPA6yGcyh4whSnU0xGJ5GAat0OfFowma9xH9gBUK5LStUiECdMmfx+mg8hmNTY-PgMYQpoZoxh41g9q6+C0GAHDyLACL5nesBkBAzBgIQ2AAG6MAA1lhcEIZgSFWvMz4VGu5YALTbtYwEnj8HhxnooT1mG3QhmY-TmJ82gGCyjzaJEsLYAK8ClOReAelRr41HRJiMZYvysexdjUuohh+poBiGDuXzGKy0HWossmKmWyqIDR3zAZWLSahM2jWJ04YjDxHbDMmmhaYE3wmemxGIchLpxOZXpWQgNEjMB1h6WEYHqK8ZgJk2EL6N8wR1Cy-gJqJ4RAA */
|
/** @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'],
|
actions: ['setSettingAtLevel', 'toastSuccess'],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'set.modeling.cameraProjection': {
|
||||||
|
target: 'persisting settings',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
'setSettingAtLevel',
|
||||||
|
'toastSuccess',
|
||||||
|
'setEngineCameraProjection',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
'set.modeling.highlightEdges': {
|
'set.modeling.highlightEdges': {
|
||||||
target: 'persisting settings',
|
target: 'persisting settings',
|
||||||
|
|
||||||
|
@ -254,6 +254,9 @@ pub struct ModelingSettings {
|
|||||||
/// The default unit to use in modeling dimensions.
|
/// The default unit to use in modeling dimensions.
|
||||||
#[serde(default, alias = "defaultUnit", skip_serializing_if = "is_default")]
|
#[serde(default, alias = "defaultUnit", skip_serializing_if = "is_default")]
|
||||||
pub base_unit: UnitLength,
|
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.
|
/// The controls for how to navigate the 3D view.
|
||||||
#[serde(default, alias = "mouseControls", skip_serializing_if = "is_default")]
|
#[serde(default, alias = "mouseControls", skip_serializing_if = "is_default")]
|
||||||
pub mouse_controls: MouseControlType,
|
pub mouse_controls: MouseControlType,
|
||||||
@ -397,6 +400,19 @@ pub enum MouseControlType {
|
|||||||
AutoCad,
|
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.
|
/// Settings that affect the behavior of the KCL text editor.
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@ -522,8 +538,8 @@ mod tests {
|
|||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AppColor, AppSettings, AppTheme, AppearanceSettings, CommandBarSettings, Configuration, ModelingSettings,
|
AppColor, AppSettings, AppTheme, AppearanceSettings, CameraProjectionType, CommandBarSettings, Configuration,
|
||||||
OnboardingStatus, ProjectSettings, Settings, TextEditorSettings, UnitLength,
|
ModelingSettings, OnboardingStatus, ProjectSettings, Settings, TextEditorSettings, UnitLength,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -538,6 +554,7 @@ enableSSAO = false
|
|||||||
|
|
||||||
[settings.modeling]
|
[settings.modeling]
|
||||||
defaultUnit = "in"
|
defaultUnit = "in"
|
||||||
|
cameraProjection = "orthographic"
|
||||||
mouseControls = "KittyCAD"
|
mouseControls = "KittyCAD"
|
||||||
showDebugPanel = true
|
showDebugPanel = true
|
||||||
|
|
||||||
@ -569,6 +586,7 @@ textWrapping = true
|
|||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::In,
|
base_unit: UnitLength::In,
|
||||||
|
camera_projection: CameraProjectionType::Orthographic,
|
||||||
mouse_controls: Default::default(),
|
mouse_controls: Default::default(),
|
||||||
highlight_edges: Default::default(),
|
highlight_edges: Default::default(),
|
||||||
show_debug_panel: true,
|
show_debug_panel: true,
|
||||||
@ -629,6 +647,7 @@ includeSettings = false
|
|||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
|
camera_projection: Default::default(),
|
||||||
mouse_controls: Default::default(),
|
mouse_controls: Default::default(),
|
||||||
highlight_edges: Default::default(),
|
highlight_edges: Default::default(),
|
||||||
show_debug_panel: true,
|
show_debug_panel: true,
|
||||||
@ -694,6 +713,7 @@ defaultProjectName = "projects-$nnn"
|
|||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
|
camera_projection: Default::default(),
|
||||||
mouse_controls: Default::default(),
|
mouse_controls: Default::default(),
|
||||||
highlight_edges: Default::default(),
|
highlight_edges: Default::default(),
|
||||||
show_debug_panel: true,
|
show_debug_panel: true,
|
||||||
@ -771,6 +791,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
|
|||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Mm,
|
base_unit: UnitLength::Mm,
|
||||||
|
camera_projection: Default::default(),
|
||||||
mouse_controls: Default::default(),
|
mouse_controls: Default::default(),
|
||||||
highlight_edges: true.into(),
|
highlight_edges: true.into(),
|
||||||
show_debug_panel: false,
|
show_debug_panel: false,
|
||||||
|
@ -127,6 +127,7 @@ includeSettings = false
|
|||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
|
camera_projection: Default::default(),
|
||||||
mouse_controls: Default::default(),
|
mouse_controls: Default::default(),
|
||||||
highlight_edges: Default::default(),
|
highlight_edges: Default::default(),
|
||||||
show_debug_panel: true,
|
show_debug_panel: true,
|
||||||
|