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,37 +168,43 @@ function CommandArgOptionInput({
 | 
			
		||||
            autoFocus
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <Combobox.Options
 | 
			
		||||
          static
 | 
			
		||||
          className="overflow-y-auto max-h-96 cursor-pointer"
 | 
			
		||||
          onMouseDown={() => {
 | 
			
		||||
            setShouldSubmitOnChange(true)
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {filteredOptions?.map((option) => (
 | 
			
		||||
            <Combobox.Option
 | 
			
		||||
              key={option.name}
 | 
			
		||||
              value={option}
 | 
			
		||||
              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') ||
 | 
			
		||||
                  ''
 | 
			
		||||
                }`}
 | 
			
		||||
        {filteredOptions?.length ? (
 | 
			
		||||
          <Combobox.Options
 | 
			
		||||
            static
 | 
			
		||||
            className="overflow-y-auto max-h-96 cursor-pointer"
 | 
			
		||||
            onMouseDown={() => {
 | 
			
		||||
              setShouldSubmitOnChange(true)
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {filteredOptions?.map((option) => (
 | 
			
		||||
              <Combobox.Option
 | 
			
		||||
                key={option.name}
 | 
			
		||||
                value={option}
 | 
			
		||||
                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"
 | 
			
		||||
              >
 | 
			
		||||
                {option.name}
 | 
			
		||||
              </p>
 | 
			
		||||
              {option.value === currentOption?.value && (
 | 
			
		||||
                <small className="text-chalkboard-70 dark:text-chalkboard-50">
 | 
			
		||||
                  current
 | 
			
		||||
                </small>
 | 
			
		||||
              )}
 | 
			
		||||
            </Combobox.Option>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Combobox.Options>
 | 
			
		||||
                <p
 | 
			
		||||
                  className={`flex-grow ${
 | 
			
		||||
                    (option.disabled &&
 | 
			
		||||
                      'text-chalkboard-70 dark:text-chalkboard-50 cursor-not-allowed') ||
 | 
			
		||||
                    ''
 | 
			
		||||
                  }`}
 | 
			
		||||
                >
 | 
			
		||||
                  {option.name}
 | 
			
		||||
                </p>
 | 
			
		||||
                {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>
 | 
			
		||||
    </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