Feature: Named views (#5532)

* feature: building skeleton for adding a viewpoint in frontend as well as rust with the settings toml

* chore: named views loaded into memory

* fix: testing code

* chore: saving off progress, skeleton for listing and deleting named views

* fix: fixed state stale dereferencing issue

* feat: initial skeleton for loading view points

* fix: pushing bug

* fix: saving off progress

* fix: trying to update to main?

* fix: main fixes, API fixes

* fix: what is happening

* fix: ope

* fix: implemented default values on serde

* fix: pushing working dev code... need to clean it up

* feature: adding no results found on filteroptions within an options input, not just command input bar level

* fix: initial PR cleanup pass of junky code

* fix: addressing comments in initial pass

* fix: addressing PR comments

* fix: moved modeling.namedViews to app.namedViews as per request

* fix: _id and _version are now id and version.

* fix: python codespell, perspective

* fix: cargo fmt

* fix: updating description of the named view commands

* fix: removing testing code

* fix: feature flag this to DEV only

* fix: ts ignore for production engine api

* fix: deep parital fights arrays and objects within settings, doing a namedview type predicate checking

* fix: auto fixes

* Remove unnecessary alias

* Reword toast messages (more consistency)

* fmt

* cargo clippy

* Fix Set appearance flakes

* cargo test

* fix: removing pub since the toml_stringify was refactored

* fix: adding ignore this on user level

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* chore: Vec<NamedView> to HashMap<uuid::Uuid,NamedView>

* fix: removing debugging code

* chore: HashMap to IndexMap

* fix: remove testing code

---------

Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Kevin Nadro
2025-02-28 15:37:25 -06:00
committed by GitHub
parent a91208eb1c
commit f238f3882b
12 changed files with 585 additions and 48 deletions

View File

@ -2864,7 +2864,7 @@ extrude001 = extrude(profile001, length = 100)
// One dumb hardcoded screen pixel value
const testPoint = { x: 500, y: 250 }
const initialColor: [number, number, number] = [135, 135, 135]
const initialColor: [number, number, number] = [123, 123, 123]
await test.step(`Confirm extrude exists with default appearance`, async () => {
await toolbar.closePane('code')
@ -2905,7 +2905,7 @@ extrude001 = extrude(profile001, length = 100)
})
await cmdBar.progressCmdBar()
await toolbar.closePane('feature-tree')
await scene.expectPixelColor(shapeColor, testPoint, 40)
await scene.expectPixelColor(shapeColor, testPoint, 10)
await toolbar.openPane('code')
if (hex === 'default') {
const anyAppearanceDeclaration = `|> appearance(`
@ -2931,9 +2931,9 @@ extrude001 = extrude(profile001, length = 100)
await setApperanceAndCheck('Purple', '#FF00FF', [180, 0, 180])
await setApperanceAndCheck('Yellow', '#FFFF00', [180, 180, 0])
await setApperanceAndCheck('Black', '#000000', [0, 0, 0])
await setApperanceAndCheck('Dark Grey', '#080808', [10, 10, 10])
await setApperanceAndCheck('Light Grey', '#D3D3D3', [190, 190, 190])
await setApperanceAndCheck('White', '#FFFFFF', [200, 200, 200])
await setApperanceAndCheck('Dark Grey', '#080808', [0x33, 0x33, 0x33])
await setApperanceAndCheck('Light Grey', '#D3D3D3', [176, 176, 176])
await setApperanceAndCheck('White', '#FFFFFF', [184, 184, 184])
await setApperanceAndCheck(
'Default (clear appearance)',
'default',

View File

@ -287,12 +287,14 @@ export class CameraControls {
camSettings.up.y,
camSettings.up.z
)
this.camera.quaternion.set(
orientation.x,
orientation.y,
orientation.z,
orientation.w
)
this.camera.up.copy(newUp)
this.camera.updateProjectionMatrix()
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {

View File

@ -168,6 +168,7 @@ function CommandArgOptionInput({
autoFocus
/>
</div>
{filteredOptions?.length ? (
<Combobox.Options
static
className="overflow-y-auto max-h-96 cursor-pointer"
@ -199,6 +200,11 @@ function CommandArgOptionInput({
</Combobox.Option>
))}
</Combobox.Options>
) : (
<p className="px-4 pt-2 text-chalkboard-60 dark:text-chalkboard-50">
No results found
</p>
)}
</Combobox>
</form>
)

View File

@ -4,6 +4,7 @@ import { type IndexLoaderData } from 'lib/types'
import { BROWSER_PATH, PATHS } from 'lib/paths'
import React, { createContext, useEffect, useMemo } from 'react'
import { toast } from 'react-hot-toast'
import { DEV } from 'env'
import {
Actor,
AnyStateMachine,
@ -32,6 +33,7 @@ import { commandBarActor } from 'machines/commandBarMachine'
import { settingsActor, useSettings } from 'machines/appMachine'
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
import { useToken } from 'machines/appMachine'
import { createNamedViewsCommand } from 'lib/commandBarConfigs/namedViewsConfig'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -58,6 +60,38 @@ export const FileMachineProvider = ({
[]
)
useEffect(() => {
// TODO: Engine feature is not deployed
if (DEV) {
const {
createNamedViewCommand,
deleteNamedViewCommand,
loadNamedViewCommand,
} = createNamedViewsCommand()
const commands = [
createNamedViewCommand,
deleteNamedViewCommand,
loadNamedViewCommand,
]
commandBarActor.send({
type: 'Add commands',
data: {
commands,
},
})
return () => {
// Remove commands if you go to the home page
commandBarActor.send({
type: 'Remove commands',
data: {
commands,
},
})
}
}
}, [])
// Due to the route provider, i've moved this to the FileMachineProvider instead of CommandBarProvider
// This will register the commands to route to Telemetry, Home, and Settings.
useEffect(() => {

View File

@ -0,0 +1,229 @@
import { NamedView } from 'wasm-lib/kcl/bindings/NamedView'
import { Command, CommandArgumentOption } from '../commandTypes'
import toast from 'react-hot-toast'
import { engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { settingsActor } from 'machines/appMachine'
import { reportRejection } from 'lib/trap'
export function createNamedViewsCommand() {
// Creates a command to be registered in the command bar.
// The createNamedViewsCommand will prompt the user for a name and then
// hit the engine for the camera properties and write them back to disk
// in project.toml.
const createNamedViewCommand: Command = {
name: 'Create named view',
displayName: `Create named view`,
description:
'Saves a named view based on your current view to load again later',
groupId: 'namedViews',
icon: 'settings',
needsReview: false,
onSubmit: (data) => {
const invokeAndForgetCreateNamedView = async () => {
if (!data) {
return toast.error('Unable to create named view, missing name')
}
// Retrieve camera view state from the engine
const cameraGetViewResponse =
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
// @ts-ignore TODO: Not in production yet.
cmd: { type: 'default_camera_get_view' },
})
if (!(cameraGetViewResponse && 'resp' in cameraGetViewResponse)) {
return toast.error('Unable to create named view, websocket failure')
}
if ('modeling_response' in cameraGetViewResponse.resp.data) {
// @ts-ignore TODO: Not in production yet.
const view = cameraGetViewResponse.resp.data.modeling_response.data
// Create a new named view
const requestedView: NamedView = {
name: data.name,
...view.view,
}
// Retrieve application state for namedViews
const namedViews = {
...settingsActor.getSnapshot().context.app.namedViews.current,
}
// Create and set namedViews application state
const uniqueUuidV4 = uuidv4()
const requestedNamedViews = {
...namedViews,
[uniqueUuidV4]: requestedView,
}
settingsActor.send({
type: `set.app.namedViews`,
data: {
level: 'project',
value: requestedNamedViews,
},
})
toast.success(`Named view ${requestedView.name} created.`)
}
}
invokeAndForgetCreateNamedView().catch(reportRejection)
},
args: {
name: {
required: true,
inputType: 'string',
},
},
}
// Given a named view selection from the command bar, this will
// find it in the setting state, remove it from the array and
// rewrite the project.toml settings to disk to delete the named view
const deleteNamedViewCommand: Command = {
name: 'Delete named view',
displayName: `Delete named view`,
description: 'Deletes the named view from settings',
groupId: 'namedViews',
icon: 'settings',
needsReview: false,
onSubmit: (data) => {
if (!data) {
return toast.error('Unable to delete named view, missing name')
}
const idToDelete = data.name
// Retrieve application state for namedViews
const namedViews = {
...settingsActor.getSnapshot().context.app.namedViews.current,
}
const { [idToDelete]: viewToDelete, ...rest } = namedViews
// Find the named view in the array
if (idToDelete && viewToDelete) {
// Update global state with the new computed state
settingsActor.send({
type: `set.app.namedViews`,
data: {
level: 'project',
value: rest,
},
})
toast.success(`Named view ${viewToDelete.name} removed.`)
} else {
toast.error(`Unable to delete, could not find the named view`)
}
},
args: {
name: {
required: true,
inputType: 'options',
options: () => {
const namedViews = {
...settingsActor.getSnapshot().context.app.namedViews.current,
}
const options: CommandArgumentOption<any>[] = []
Object.entries(namedViews).forEach(([key, view]) => {
if (view) {
options.push({
name: view.name,
isCurrent: false,
value: key,
})
}
})
return options
},
},
},
}
// Read the named view from settings state and pass that camera information to the engine command to set the view of the engine camera
const loadNamedViewCommand: Command = {
name: 'Load named view',
displayName: `Load named view`,
description: 'Loads your camera to the named view',
groupId: 'namedViews',
icon: 'settings',
needsReview: false,
onSubmit: (data) => {
const invokeAndForgetLoadNamedView = async () => {
if (!data) {
return toast.error('Unable to load named view')
}
// Retrieve application state for namedViews
const namedViews = {
...settingsActor.getSnapshot().context.app.namedViews.current,
}
const idToLoad = data.name
const viewToLoad = namedViews[idToLoad]
if (viewToLoad) {
// Split into the name and the engine data
const { name, version, ...engineViewData } = viewToLoad
// Only send the specific camera information, the NamedView itself
// is not directly compatible with the engine API
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
// @ts-ignore TODO: Not in production yet.
type: 'default_camera_set_view',
view: {
...engineViewData,
},
},
})
const isPerpsective = !engineViewData.is_ortho
// Update the GUI for orthographic and projection
settingsActor.send({
type: 'set.modeling.cameraProjection',
data: {
level: 'user',
value: isPerpsective ? 'perspective' : 'orthographic',
},
})
toast.success(`Named view ${name} loaded.`)
} else {
toast.error(`Unable to load named view, could not find named view`)
}
}
invokeAndForgetLoadNamedView().catch(reportRejection)
},
args: {
name: {
required: true,
inputType: 'options',
options: () => {
const namedViews = {
...settingsActor.getSnapshot().context.app.namedViews.current,
}
const options: CommandArgumentOption<any>[] = []
Object.entries(namedViews).forEach(([key, view]) => {
if (view) {
options.push({
name: view.name,
isCurrent: false,
value: key,
})
}
})
return options
},
},
},
}
return {
createNamedViewCommand,
deleteNamedViewCommand,
loadNamedViewCommand,
}
}

View File

@ -20,6 +20,7 @@ import { isArray, toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
import { NamedView } from 'wasm-lib/kcl/bindings/NamedView'
import { CameraOrbitType } from 'wasm-lib/kcl/bindings/CameraOrbitType'
/**
@ -263,6 +264,11 @@ export function createSettings() {
)
},
}),
namedViews: new Setting<{ [key in string]: NamedView }>({
defaultValue: {},
validate: (v) => true,
hideOnLevel: 'user',
}),
},
/**
* Settings that affect the behavior while modeling.

View File

@ -23,6 +23,7 @@ import { err } from 'lib/trap'
import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { NamedView } from 'wasm-lib/kcl/bindings/NamedView'
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
/**
@ -72,6 +73,43 @@ export function configurationToSettingsPayload(
}
}
export function isNamedView(
namedView: DeepPartial<NamedView> | undefined
): namedView is NamedView {
const namedViewKeys = [
'name',
'eye_offset',
'fov_y',
'ortho_scale_enabled',
'ortho_scale_factor',
'pivot_position',
'pivot_rotation',
'world_coord_system',
'version',
] as const
return namedViewKeys.every((key) => {
return namedView && namedView[key]
})
}
function deepPartialNamedViewsToNamedViews(
maybeViews: { [key: string]: NamedView | undefined } | undefined
): { [key: string]: NamedView } {
const namedViews: { [key: string]: NamedView } = {}
if (!maybeViews) {
return namedViews
}
Object.entries(maybeViews)?.forEach(([key, maybeView]) => {
if (isNamedView(maybeView)) {
namedViews[key] = maybeView
}
})
return namedViews
}
export function projectConfigurationToSettingsPayload(
configuration: DeepPartial<ProjectConfiguration>
): DeepPartial<SaveSettingsPayload> {
@ -87,6 +125,9 @@ export function projectConfigurationToSettingsPayload(
allowOrbitInSketchMode:
configuration?.settings?.app?.allow_orbit_in_sketch_mode,
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
namedViews: deepPartialNamedViewsToNamedViews(
configuration?.settings?.app?.named_views
),
},
modeling: {
defaultUnit: configuration?.settings?.modeling?.base_unit,

View File

@ -35,6 +35,7 @@ import {
saveSettings,
setSettingsAtLevel,
} from 'lib/settings/settingsUtils'
import { NamedView } from 'wasm-lib/kcl/bindings/NamedView'
import {
codeManager,
engineCommandManager,
@ -77,6 +78,7 @@ export const settingsMachine = setup({
level: SettingsLevel
}
| { type: 'Set all settings'; settings: typeof settings }
| { type: 'set.app.namedViews'; value: NamedView }
| { type: 'load.project'; project?: Project }
| { type: 'clear.project' }
) & { doNotPersist?: boolean },
@ -151,6 +153,7 @@ export const settingsMachine = setup({
type: 'Add commands',
data: { commands: commands },
})
const removeCommands = () =>
commandBarActor.send({
type: 'Remove commands',
@ -391,6 +394,12 @@ export const settingsMachine = setup({
],
},
'set.app.namedViews': {
target: 'persisting settings',
actions: ['setSettingAtLevel'],
},
'set.app.onboardingStatus': {
target: 'persisting settings',

View File

@ -4220,9 +4220,9 @@ dependencies = [
[[package]]
name = "validator"
version = "0.19.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303"
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
dependencies = [
"idna",
"once_cell",
@ -4236,9 +4236,9 @@ dependencies = [
[[package]]
name = "validator_derive"
version = "0.19.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77"
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
dependencies = [
"darling",
"once_cell",

View File

@ -72,7 +72,7 @@ ts-rs = { version = "10.1.0", features = [
url = { version = "2.5.4", features = ["serde"] }
urlencoding = "2.1.3"
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
validator = { version = "0.19.0", features = ["derive"] }
validator = { version = "0.20.0", features = ["derive"] }
web-time = "1.1"
winnow = "0.6.22"
zip = { version = "2.2.2", default-features = false }
@ -140,4 +140,3 @@ required-features = ["lsp-test-util"]
[[bench]]
name = "executor_benchmark_criterion"
harness = false

View File

@ -3,6 +3,7 @@
pub mod project;
use anyhow::Result;
use indexmap::IndexMap;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -124,6 +125,9 @@ pub struct AppSettings {
/// When the user is idle, and this is true, the stream will be torn down.
#[serde(default, alias = "allowOrbitInSketchMode", skip_serializing_if = "is_default")]
allow_orbit_in_sketch_mode: bool,
/// Settings that affect the behavior of the command bar.
#[serde(default, alias = "namedViews", skip_serializing_if = "IndexMap::is_empty")]
pub named_views: IndexMap<uuid::Uuid, NamedView>,
}
// TODO: When we remove backwards compatibility with the old settings file, we can remove this.
@ -280,6 +284,46 @@ pub struct ModelingSettings {
pub show_scale_grid: bool,
}
fn named_view_point_version_one() -> f64 {
1.0
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Validate, PartialEq)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub struct NamedView {
/// User defined name to identify the named view. A label.
#[serde(default, alias = "name", skip_serializing_if = "is_default")]
pub name: String,
/// Engine camera eye off set
#[serde(default, alias = "eyeOffset", skip_serializing_if = "is_default")]
pub eye_offset: f64,
/// Engine camera vertical FOV
#[serde(default, alias = "fovY", skip_serializing_if = "is_default")]
pub fov_y: f64,
// Engine camera is orthographic or perspective projection
#[serde(default, alias = "isOrtho")]
pub is_ortho: bool,
/// Engine camera is orthographic camera scaling enabled
#[serde(default, alias = "orthoScaleEnabled")]
pub ortho_scale_enabled: bool,
/// Engine camera orthographic scaling factor
#[serde(default, alias = "orthoScaleFactor", skip_serializing_if = "is_default")]
pub ortho_scale_factor: f64,
/// Engine camera position that the camera pivots around
#[serde(default, alias = "pivotPosition", skip_serializing_if = "is_default")]
pub pivot_position: [f64; 3],
/// Engine camera orientation in relation to the pivot position
#[serde(default, alias = "pivotRotation", skip_serializing_if = "is_default")]
pub pivot_rotation: [f64; 4],
/// Engine camera world coordinate system orientation
#[serde(default, alias = "worldCoordSystem", skip_serializing_if = "is_default")]
pub world_coord_system: String,
/// Version number of the view point if the engine camera API changes
#[serde(default = "named_view_point_version_one")]
pub version: f64,
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(transparent)]
@ -566,6 +610,7 @@ mod tests {
ModelingSettings, OnboardingStatus, ProjectSettings, Settings, TextEditorSettings, UnitLength,
};
use crate::settings::types::CameraOrbitType;
use indexmap::IndexMap;
#[test]
// Test that we can deserialize a project file from the old format.
@ -609,6 +654,7 @@ textWrapping = true
enable_ssao: None,
stream_idle_mode: false,
allow_orbit_in_sketch_mode: false,
named_views: IndexMap::default()
},
modeling: ModelingSettings {
base_unit: UnitLength::In,
@ -672,6 +718,7 @@ includeSettings = false
enable_ssao: None,
stream_idle_mode: false,
allow_orbit_in_sketch_mode: false,
named_views: IndexMap::default()
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
@ -740,6 +787,7 @@ defaultProjectName = "projects-$nnn"
enable_ssao: None,
stream_idle_mode: false,
allow_orbit_in_sketch_mode: false,
named_views: IndexMap::default()
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
@ -820,6 +868,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
enable_ssao: None,
stream_idle_mode: false,
allow_orbit_in_sketch_mode: false,
named_views: IndexMap::default()
},
modeling: ModelingSettings {
base_unit: UnitLength::Mm,

View File

@ -84,7 +84,10 @@ mod tests {
AppSettings, AppTheme, CommandBarSettings, ModelingSettings, PerProjectSettings, ProjectConfiguration,
TextEditorSettings,
};
use crate::settings::types::{AppearanceSettings, UnitLength};
use crate::settings::types::{AppearanceSettings, NamedView, UnitLength};
use indexmap::IndexMap;
use serde_json::Value;
#[test]
// Test that we can deserialize a project file from the old format.
@ -94,10 +97,6 @@ mod tests {
theme = "dark"
themeColor = "138"
[settings.modeling]
defaultUnit = "yd"
showDebugPanel = true
[settings.textEditor]
textWrapping = false
blinkingCursor = false
@ -125,14 +124,15 @@ includeSettings = false
enable_ssao: None,
stream_idle_mode: false,
allow_orbit_in_sketch_mode: false,
named_views: IndexMap::default()
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
base_unit: UnitLength::Mm,
camera_projection: Default::default(),
camera_orbit: Default::default(),
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
show_debug_panel: false,
enable_ssao: true.into(),
show_scale_grid: false,
},
@ -189,4 +189,166 @@ color = 1567.4"#;
.to_string()
.contains("color: Validation error: color"));
}
#[test]
fn named_view_serde_json() {
let json = r#"
[
{
"name":"dog",
"pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
"pivot_position":[0.5,0,0.5],
"eye_offset":231.52048,
"fov_y":45,
"ortho_scale_factor":1.574129,
"is_ortho":true,
"ortho_scale_enabled":true,
"world_coord_system":"RightHandedUpZ"
}
]
"#;
// serde_json to a NamedView will produce default values
let named_views: Vec<NamedView> = serde_json::from_str(json).unwrap();
let version = named_views[0].version;
assert_eq!(version, 1.0);
}
#[test]
fn named_view_serde_json_string() {
let json = r#"
[
{
"name":"dog",
"pivot_rotation":[0.53809947,0.0,0.0,0.8428814],
"pivot_position":[0.5,0,0.5],
"eye_offset":231.52048,
"fov_y":45,
"ortho_scale_factor":1.574129,
"is_ortho":true,
"ortho_scale_enabled":true,
"world_coord_system":"RightHandedUpZ"
}
]
"#;
// serde_json to string does not produce default values
let named_views: Value = match serde_json::from_str(json) {
Ok(x) => x,
Err(_) => return,
};
println!("{}", named_views);
}
#[test]
fn test_project_settings_named_views() {
let conf = ProjectConfiguration {
settings: PerProjectSettings {
app: AppSettings {
appearance: AppearanceSettings {
theme: AppTheme::Dark,
color: 138.0.into(),
},
onboarding_status: Default::default(),
project_directory: None,
theme: None,
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
allow_orbit_in_sketch_mode: false,
named_views: IndexMap::from([
(
uuid::uuid!("323611ea-66e3-43c9-9d0d-1091ba92948c"),
NamedView {
name: String::from("Hello"),
eye_offset: 1236.4015,
fov_y: 45.0,
is_ortho: false,
ortho_scale_enabled: false,
ortho_scale_factor: 45.0,
pivot_position: [-100.0, 100.0, 100.0],
pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
world_coord_system: String::from("RightHandedUpZ"),
version: 1.0,
},
),
(
uuid::uuid!("423611ea-66e3-43c9-9d0d-1091ba92948c"),
NamedView {
name: String::from("Goodbye"),
eye_offset: 1236.4015,
fov_y: 45.0,
is_ortho: false,
ortho_scale_enabled: false,
ortho_scale_factor: 45.0,
pivot_position: [-100.0, 100.0, 100.0],
pivot_rotation: [-0.16391756, 0.9862819, -0.01956843, 0.0032552152],
world_coord_system: String::from("RightHandedUpZ"),
version: 1.0,
},
),
]),
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
camera_projection: Default::default(),
camera_orbit: Default::default(),
mouse_controls: Default::default(),
highlight_edges: Default::default(),
show_debug_panel: true,
enable_ssao: true.into(),
show_scale_grid: false,
},
text_editor: TextEditorSettings {
text_wrapping: false.into(),
blinking_cursor: false.into(),
},
command_bar: CommandBarSettings {
include_settings: false.into(),
},
},
};
let serialized = toml::to_string(&conf).unwrap();
let old_project_file = r#"[settings.app.appearance]
theme = "dark"
color = 138.0
[settings.app.named_views.323611ea-66e3-43c9-9d0d-1091ba92948c]
name = "Hello"
eye_offset = 1236.4015
fov_y = 45.0
is_ortho = false
ortho_scale_enabled = false
ortho_scale_factor = 45.0
pivot_position = [-100.0, 100.0, 100.0]
pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
world_coord_system = "RightHandedUpZ"
version = 1.0
[settings.app.named_views.423611ea-66e3-43c9-9d0d-1091ba92948c]
name = "Goodbye"
eye_offset = 1236.4015
fov_y = 45.0
is_ortho = false
ortho_scale_enabled = false
ortho_scale_factor = 45.0
pivot_position = [-100.0, 100.0, 100.0]
pivot_rotation = [-0.16391756, 0.9862819, -0.01956843, 0.0032552152]
world_coord_system = "RightHandedUpZ"
version = 1.0
[settings.modeling]
base_unit = "yd"
show_debug_panel = true
[settings.text_editor]
text_wrapping = false
blinking_cursor = false
[settings.command_bar]
include_settings = false
"#;
assert_eq!(serialized, old_project_file)
}
}