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:
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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(() => {
|
||||
|
229
src/lib/commandBarConfigs/namedViewsConfig.ts
Normal file
229
src/lib/commandBarConfigs/namedViewsConfig.ts
Normal 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,
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
||||
|
8
src/wasm-lib/Cargo.lock
generated
8
src/wasm-lib/Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user