From f238f3882bb4fa47d56d28cbdfdfd347972dede2 Mon Sep 17 00:00:00 2001 From: Kevin Nadro Date: Fri, 28 Feb 2025 15:37:25 -0600 Subject: [PATCH] Feature: Named views (#5532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 to HashMap * fix: removing debugging code * chore: HashMap to IndexMap * fix: remove testing code --------- Co-authored-by: 49lf Co-authored-by: github-actions[bot] --- e2e/playwright/point-click.spec.ts | 10 +- src/clientSideScene/CameraControls.ts | 2 + .../CommandBar/CommandArgOptionInput.tsx | 66 ++--- src/components/FileMachineProvider.tsx | 34 +++ src/lib/commandBarConfigs/namedViewsConfig.ts | 229 ++++++++++++++++++ src/lib/settings/initialSettings.tsx | 6 + src/lib/settings/settingsUtils.ts | 41 ++++ src/machines/settingsMachine.ts | 9 + src/wasm-lib/Cargo.lock | 8 +- src/wasm-lib/kcl/Cargo.toml | 3 +- src/wasm-lib/kcl/src/settings/types/mod.rs | 49 ++++ .../kcl/src/settings/types/project.rs | 176 +++++++++++++- 12 files changed, 585 insertions(+), 48 deletions(-) create mode 100644 src/lib/commandBarConfigs/namedViewsConfig.ts diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index 4fe637ad5..5f67b20de 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -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', diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index 9d16d9cc6..709981ca2 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -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) { diff --git a/src/components/CommandBar/CommandArgOptionInput.tsx b/src/components/CommandBar/CommandArgOptionInput.tsx index c1d715af6..386c5f4ad 100644 --- a/src/components/CommandBar/CommandArgOptionInput.tsx +++ b/src/components/CommandBar/CommandArgOptionInput.tsx @@ -168,37 +168,43 @@ function CommandArgOptionInput({ autoFocus /> - { - setShouldSubmitOnChange(true) - }} - > - {filteredOptions?.map((option) => ( - -

{ + setShouldSubmitOnChange(true) + }} + > + {filteredOptions?.map((option) => ( + - {option.name} -

- {option.value === currentOption?.value && ( - - current - - )} -
- ))} -
+

+ {option.name} +

+ {option.value === currentOption?.value && ( + + current + + )} + + ))} + + ) : ( +

+ No results found +

+ )} ) diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx index 3357be28c..59c5ba45c 100644 --- a/src/components/FileMachineProvider.tsx +++ b/src/components/FileMachineProvider.tsx @@ -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 = { state: StateFrom @@ -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(() => { diff --git a/src/lib/commandBarConfigs/namedViewsConfig.ts b/src/lib/commandBarConfigs/namedViewsConfig.ts new file mode 100644 index 000000000..5b88f707c --- /dev/null +++ b/src/lib/commandBarConfigs/namedViewsConfig.ts @@ -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[] = [] + 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[] = [] + Object.entries(namedViews).forEach(([key, view]) => { + if (view) { + options.push({ + name: view.name, + isCurrent: false, + value: key, + }) + } + }) + return options + }, + }, + }, + } + + return { + createNamedViewCommand, + deleteNamedViewCommand, + loadNamedViewCommand, + } +} diff --git a/src/lib/settings/initialSettings.tsx b/src/lib/settings/initialSettings.tsx index c8f9f283e..7c91d5d22 100644 --- a/src/lib/settings/initialSettings.tsx +++ b/src/lib/settings/initialSettings.tsx @@ -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. diff --git a/src/lib/settings/settingsUtils.ts b/src/lib/settings/settingsUtils.ts index deffa9690..3349cda8a 100644 --- a/src/lib/settings/settingsUtils.ts +++ b/src/lib/settings/settingsUtils.ts @@ -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 | 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 ): DeepPartial { @@ -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, diff --git a/src/machines/settingsMachine.ts b/src/machines/settingsMachine.ts index a1f01c043..adad55209 100644 --- a/src/machines/settingsMachine.ts +++ b/src/machines/settingsMachine.ts @@ -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', diff --git a/src/wasm-lib/Cargo.lock b/src/wasm-lib/Cargo.lock index 04575007c..b6e4228fd 100644 --- a/src/wasm-lib/Cargo.lock +++ b/src/wasm-lib/Cargo.lock @@ -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", diff --git a/src/wasm-lib/kcl/Cargo.toml b/src/wasm-lib/kcl/Cargo.toml index 34a8c7589..55d02de67 100644 --- a/src/wasm-lib/kcl/Cargo.toml +++ b/src/wasm-lib/kcl/Cargo.toml @@ -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 - diff --git a/src/wasm-lib/kcl/src/settings/types/mod.rs b/src/wasm-lib/kcl/src/settings/types/mod.rs index aa7059bcf..2727b0af2 100644 --- a/src/wasm-lib/kcl/src/settings/types/mod.rs +++ b/src/wasm-lib/kcl/src/settings/types/mod.rs @@ -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, } // 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, diff --git a/src/wasm-lib/kcl/src/settings/types/project.rs b/src/wasm-lib/kcl/src/settings/types/project.rs index 533908c9b..ff61b212d 100644 --- a/src/wasm-lib/kcl/src/settings/types/project.rs +++ b/src/wasm-lib/kcl/src/settings/types/project.rs @@ -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 = 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) + } }