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
|
// One dumb hardcoded screen pixel value
|
||||||
const testPoint = { x: 500, y: 250 }
|
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 test.step(`Confirm extrude exists with default appearance`, async () => {
|
||||||
await toolbar.closePane('code')
|
await toolbar.closePane('code')
|
||||||
@ -2905,7 +2905,7 @@ extrude001 = extrude(profile001, length = 100)
|
|||||||
})
|
})
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await toolbar.closePane('feature-tree')
|
await toolbar.closePane('feature-tree')
|
||||||
await scene.expectPixelColor(shapeColor, testPoint, 40)
|
await scene.expectPixelColor(shapeColor, testPoint, 10)
|
||||||
await toolbar.openPane('code')
|
await toolbar.openPane('code')
|
||||||
if (hex === 'default') {
|
if (hex === 'default') {
|
||||||
const anyAppearanceDeclaration = `|> appearance(`
|
const anyAppearanceDeclaration = `|> appearance(`
|
||||||
@ -2931,9 +2931,9 @@ extrude001 = extrude(profile001, length = 100)
|
|||||||
await setApperanceAndCheck('Purple', '#FF00FF', [180, 0, 180])
|
await setApperanceAndCheck('Purple', '#FF00FF', [180, 0, 180])
|
||||||
await setApperanceAndCheck('Yellow', '#FFFF00', [180, 180, 0])
|
await setApperanceAndCheck('Yellow', '#FFFF00', [180, 180, 0])
|
||||||
await setApperanceAndCheck('Black', '#000000', [0, 0, 0])
|
await setApperanceAndCheck('Black', '#000000', [0, 0, 0])
|
||||||
await setApperanceAndCheck('Dark Grey', '#080808', [10, 10, 10])
|
await setApperanceAndCheck('Dark Grey', '#080808', [0x33, 0x33, 0x33])
|
||||||
await setApperanceAndCheck('Light Grey', '#D3D3D3', [190, 190, 190])
|
await setApperanceAndCheck('Light Grey', '#D3D3D3', [176, 176, 176])
|
||||||
await setApperanceAndCheck('White', '#FFFFFF', [200, 200, 200])
|
await setApperanceAndCheck('White', '#FFFFFF', [184, 184, 184])
|
||||||
await setApperanceAndCheck(
|
await setApperanceAndCheck(
|
||||||
'Default (clear appearance)',
|
'Default (clear appearance)',
|
||||||
'default',
|
'default',
|
||||||
|
@ -287,12 +287,14 @@ export class CameraControls {
|
|||||||
camSettings.up.y,
|
camSettings.up.y,
|
||||||
camSettings.up.z
|
camSettings.up.z
|
||||||
)
|
)
|
||||||
|
|
||||||
this.camera.quaternion.set(
|
this.camera.quaternion.set(
|
||||||
orientation.x,
|
orientation.x,
|
||||||
orientation.y,
|
orientation.y,
|
||||||
orientation.z,
|
orientation.z,
|
||||||
orientation.w
|
orientation.w
|
||||||
)
|
)
|
||||||
|
|
||||||
this.camera.up.copy(newUp)
|
this.camera.up.copy(newUp)
|
||||||
this.camera.updateProjectionMatrix()
|
this.camera.updateProjectionMatrix()
|
||||||
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
||||||
|
@ -168,37 +168,43 @@ function CommandArgOptionInput({
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Combobox.Options
|
{filteredOptions?.length ? (
|
||||||
static
|
<Combobox.Options
|
||||||
className="overflow-y-auto max-h-96 cursor-pointer"
|
static
|
||||||
onMouseDown={() => {
|
className="overflow-y-auto max-h-96 cursor-pointer"
|
||||||
setShouldSubmitOnChange(true)
|
onMouseDown={() => {
|
||||||
}}
|
setShouldSubmitOnChange(true)
|
||||||
>
|
}}
|
||||||
{filteredOptions?.map((option) => (
|
>
|
||||||
<Combobox.Option
|
{filteredOptions?.map((option) => (
|
||||||
key={option.name}
|
<Combobox.Option
|
||||||
value={option}
|
key={option.name}
|
||||||
disabled={option.disabled}
|
value={option}
|
||||||
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
|
disabled={option.disabled}
|
||||||
>
|
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
|
||||||
<p
|
|
||||||
className={`flex-grow ${
|
|
||||||
(option.disabled &&
|
|
||||||
'text-chalkboard-70 dark:text-chalkboard-50 cursor-not-allowed') ||
|
|
||||||
''
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{option.name}
|
<p
|
||||||
</p>
|
className={`flex-grow ${
|
||||||
{option.value === currentOption?.value && (
|
(option.disabled &&
|
||||||
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
'text-chalkboard-70 dark:text-chalkboard-50 cursor-not-allowed') ||
|
||||||
current
|
''
|
||||||
</small>
|
}`}
|
||||||
)}
|
>
|
||||||
</Combobox.Option>
|
{option.name}
|
||||||
))}
|
</p>
|
||||||
</Combobox.Options>
|
{option.value === currentOption?.value && (
|
||||||
|
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
||||||
|
current
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
) : (
|
||||||
|
<p className="px-4 pt-2 text-chalkboard-60 dark:text-chalkboard-50">
|
||||||
|
No results found
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
@ -4,6 +4,7 @@ import { type IndexLoaderData } from 'lib/types'
|
|||||||
import { BROWSER_PATH, PATHS } from 'lib/paths'
|
import { BROWSER_PATH, PATHS } from 'lib/paths'
|
||||||
import React, { createContext, useEffect, useMemo } from 'react'
|
import React, { createContext, useEffect, useMemo } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { DEV } from 'env'
|
||||||
import {
|
import {
|
||||||
Actor,
|
Actor,
|
||||||
AnyStateMachine,
|
AnyStateMachine,
|
||||||
@ -32,6 +33,7 @@ import { commandBarActor } from 'machines/commandBarMachine'
|
|||||||
import { settingsActor, useSettings } from 'machines/appMachine'
|
import { settingsActor, useSettings } from 'machines/appMachine'
|
||||||
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
|
||||||
import { useToken } from 'machines/appMachine'
|
import { useToken } from 'machines/appMachine'
|
||||||
|
import { createNamedViewsCommand } from 'lib/commandBarConfigs/namedViewsConfig'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
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
|
// 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.
|
// This will register the commands to route to Telemetry, Home, and Settings.
|
||||||
useEffect(() => {
|
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 { reportRejection } from 'lib/trap'
|
||||||
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
|
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
|
||||||
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
|
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
|
||||||
|
import { NamedView } from 'wasm-lib/kcl/bindings/NamedView'
|
||||||
import { CameraOrbitType } from 'wasm-lib/kcl/bindings/CameraOrbitType'
|
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.
|
* Settings that affect the behavior while modeling.
|
||||||
|
@ -23,6 +23,7 @@ import { err } from 'lib/trap'
|
|||||||
import { DeepPartial } from 'lib/types'
|
import { DeepPartial } from 'lib/types'
|
||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||||
|
import { NamedView } from 'wasm-lib/kcl/bindings/NamedView'
|
||||||
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
|
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(
|
export function projectConfigurationToSettingsPayload(
|
||||||
configuration: DeepPartial<ProjectConfiguration>
|
configuration: DeepPartial<ProjectConfiguration>
|
||||||
): DeepPartial<SaveSettingsPayload> {
|
): DeepPartial<SaveSettingsPayload> {
|
||||||
@ -87,6 +125,9 @@ export function projectConfigurationToSettingsPayload(
|
|||||||
allowOrbitInSketchMode:
|
allowOrbitInSketchMode:
|
||||||
configuration?.settings?.app?.allow_orbit_in_sketch_mode,
|
configuration?.settings?.app?.allow_orbit_in_sketch_mode,
|
||||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||||
|
namedViews: deepPartialNamedViewsToNamedViews(
|
||||||
|
configuration?.settings?.app?.named_views
|
||||||
|
),
|
||||||
},
|
},
|
||||||
modeling: {
|
modeling: {
|
||||||
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
saveSettings,
|
saveSettings,
|
||||||
setSettingsAtLevel,
|
setSettingsAtLevel,
|
||||||
} from 'lib/settings/settingsUtils'
|
} from 'lib/settings/settingsUtils'
|
||||||
|
import { NamedView } from 'wasm-lib/kcl/bindings/NamedView'
|
||||||
import {
|
import {
|
||||||
codeManager,
|
codeManager,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
@ -77,6 +78,7 @@ export const settingsMachine = setup({
|
|||||||
level: SettingsLevel
|
level: SettingsLevel
|
||||||
}
|
}
|
||||||
| { type: 'Set all settings'; settings: typeof settings }
|
| { type: 'Set all settings'; settings: typeof settings }
|
||||||
|
| { type: 'set.app.namedViews'; value: NamedView }
|
||||||
| { type: 'load.project'; project?: Project }
|
| { type: 'load.project'; project?: Project }
|
||||||
| { type: 'clear.project' }
|
| { type: 'clear.project' }
|
||||||
) & { doNotPersist?: boolean },
|
) & { doNotPersist?: boolean },
|
||||||
@ -151,6 +153,7 @@ export const settingsMachine = setup({
|
|||||||
type: 'Add commands',
|
type: 'Add commands',
|
||||||
data: { commands: commands },
|
data: { commands: commands },
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeCommands = () =>
|
const removeCommands = () =>
|
||||||
commandBarActor.send({
|
commandBarActor.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
@ -391,6 +394,12 @@ export const settingsMachine = setup({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'set.app.namedViews': {
|
||||||
|
target: 'persisting settings',
|
||||||
|
|
||||||
|
actions: ['setSettingAtLevel'],
|
||||||
|
},
|
||||||
|
|
||||||
'set.app.onboardingStatus': {
|
'set.app.onboardingStatus': {
|
||||||
target: 'persisting settings',
|
target: 'persisting settings',
|
||||||
|
|
||||||
|
8
src/wasm-lib/Cargo.lock
generated
8
src/wasm-lib/Cargo.lock
generated
@ -4220,9 +4220,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "validator"
|
name = "validator"
|
||||||
version = "0.19.0"
|
version = "0.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303"
|
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"idna",
|
"idna",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -4236,9 +4236,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "validator_derive"
|
name = "validator_derive"
|
||||||
version = "0.19.0"
|
version = "0.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77"
|
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
@ -72,7 +72,7 @@ ts-rs = { version = "10.1.0", features = [
|
|||||||
url = { version = "2.5.4", features = ["serde"] }
|
url = { version = "2.5.4", features = ["serde"] }
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
|
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"
|
web-time = "1.1"
|
||||||
winnow = "0.6.22"
|
winnow = "0.6.22"
|
||||||
zip = { version = "2.2.2", default-features = false }
|
zip = { version = "2.2.2", default-features = false }
|
||||||
@ -140,4 +140,3 @@ required-features = ["lsp-test-util"]
|
|||||||
[[bench]]
|
[[bench]]
|
||||||
name = "executor_benchmark_criterion"
|
name = "executor_benchmark_criterion"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
pub mod project;
|
pub mod project;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use indexmap::IndexMap;
|
||||||
use parse_display::{Display, FromStr};
|
use parse_display::{Display, FromStr};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
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.
|
/// When the user is idle, and this is true, the stream will be torn down.
|
||||||
#[serde(default, alias = "allowOrbitInSketchMode", skip_serializing_if = "is_default")]
|
#[serde(default, alias = "allowOrbitInSketchMode", skip_serializing_if = "is_default")]
|
||||||
allow_orbit_in_sketch_mode: bool,
|
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.
|
// 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,
|
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)]
|
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
@ -566,6 +610,7 @@ mod tests {
|
|||||||
ModelingSettings, OnboardingStatus, ProjectSettings, Settings, TextEditorSettings, UnitLength,
|
ModelingSettings, OnboardingStatus, ProjectSettings, Settings, TextEditorSettings, UnitLength,
|
||||||
};
|
};
|
||||||
use crate::settings::types::CameraOrbitType;
|
use crate::settings::types::CameraOrbitType;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// Test that we can deserialize a project file from the old format.
|
// Test that we can deserialize a project file from the old format.
|
||||||
@ -609,6 +654,7 @@ textWrapping = true
|
|||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: false,
|
||||||
allow_orbit_in_sketch_mode: false,
|
allow_orbit_in_sketch_mode: false,
|
||||||
|
named_views: IndexMap::default()
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::In,
|
base_unit: UnitLength::In,
|
||||||
@ -672,6 +718,7 @@ includeSettings = false
|
|||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: false,
|
||||||
allow_orbit_in_sketch_mode: false,
|
allow_orbit_in_sketch_mode: false,
|
||||||
|
named_views: IndexMap::default()
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
@ -740,6 +787,7 @@ defaultProjectName = "projects-$nnn"
|
|||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: false,
|
||||||
allow_orbit_in_sketch_mode: false,
|
allow_orbit_in_sketch_mode: false,
|
||||||
|
named_views: IndexMap::default()
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
@ -820,6 +868,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
|
|||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: false,
|
||||||
allow_orbit_in_sketch_mode: false,
|
allow_orbit_in_sketch_mode: false,
|
||||||
|
named_views: IndexMap::default()
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Mm,
|
base_unit: UnitLength::Mm,
|
||||||
|
@ -84,7 +84,10 @@ mod tests {
|
|||||||
AppSettings, AppTheme, CommandBarSettings, ModelingSettings, PerProjectSettings, ProjectConfiguration,
|
AppSettings, AppTheme, CommandBarSettings, ModelingSettings, PerProjectSettings, ProjectConfiguration,
|
||||||
TextEditorSettings,
|
TextEditorSettings,
|
||||||
};
|
};
|
||||||
use crate::settings::types::{AppearanceSettings, UnitLength};
|
use crate::settings::types::{AppearanceSettings, NamedView, UnitLength};
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// Test that we can deserialize a project file from the old format.
|
// Test that we can deserialize a project file from the old format.
|
||||||
@ -94,10 +97,6 @@ mod tests {
|
|||||||
theme = "dark"
|
theme = "dark"
|
||||||
themeColor = "138"
|
themeColor = "138"
|
||||||
|
|
||||||
[settings.modeling]
|
|
||||||
defaultUnit = "yd"
|
|
||||||
showDebugPanel = true
|
|
||||||
|
|
||||||
[settings.textEditor]
|
[settings.textEditor]
|
||||||
textWrapping = false
|
textWrapping = false
|
||||||
blinkingCursor = false
|
blinkingCursor = false
|
||||||
@ -125,14 +124,15 @@ includeSettings = false
|
|||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: false,
|
||||||
allow_orbit_in_sketch_mode: false,
|
allow_orbit_in_sketch_mode: false,
|
||||||
|
named_views: IndexMap::default()
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Mm,
|
||||||
camera_projection: Default::default(),
|
camera_projection: Default::default(),
|
||||||
camera_orbit: Default::default(),
|
camera_orbit: 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: false,
|
||||||
enable_ssao: true.into(),
|
enable_ssao: true.into(),
|
||||||
show_scale_grid: false,
|
show_scale_grid: false,
|
||||||
},
|
},
|
||||||
@ -189,4 +189,166 @@ color = 1567.4"#;
|
|||||||
.to_string()
|
.to_string()
|
||||||
.contains("color: Validation error: color"));
|
.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