Feature: Release named views to all users (#5814)

* chore: cleanup to get named views released!

* fix: fixed gizmo, client side camera sync and remove DEV flag

* yarp

* chore: implementing E2E tests for creating a named view

* fix: cleaning up and commenting E2E tests for named views

* fix: we did it bois, the skip ceral i zation bricked my E2E test :(

* fix: auto formatter

* fix: snapshot uuid matching because rust will randomly generate thme

* fix: auto fmt

* fix: trying to resolve typescript issues

* fix: handling NamedView vs CameraViewState type checking

* fix: no idea I just mapped export to 3d export because we have no 2d export yet...

* fix: random file I wrote because my editor was too slow

* fix: git merge did not do what I wanted

* A snapshot a day keeps the bugs away! 📷🐛

* fix: linter errors

* A snapshot a day keeps the bugs away! 📷🐛

---------

Co-authored-by: 49fl <ircsurfer33@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
This commit is contained in:
Kevin Nadro
2025-03-26 11:12:35 -05:00
committed by GitHub
parent fa16fcedff
commit 1753047d87
12 changed files with 538 additions and 70 deletions

View File

@ -0,0 +1,292 @@
import { test, expect } from './zoo-test'
import { PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import * as fsp from 'fs/promises'
import { join } from 'path'
import {
createProject,
tomlToPerProjectSettings,
perProjectsettingsToToml,
} from './test-utils'
import { NamedView } from '@rust/kcl-lib/bindings/NamedView'
// Helper function to determine if the file path on disk exists
// Specifically this is used to check if project.toml exists on disk
const fileExists = async (path: string) => {
return !!(await fsp
.stat(path)
.then((_) => true)
.catch((_) => false))
}
// Here are a few uuids.
// When created named views rust will auto generate uuids and they will
// never match the snapshots. Overwrite them in memory to these
// values to have them match the snapshots.
const uuid1: string = '0656fb1a-9640-473e-b334-591dc70c0138'
const uuid2: string = 'c810cf04-c6cc-4a4a-8b11-17bf445dcab7'
const uuid3: string = 'cfecbfee-48a6-4561-b96d-ffbe5678bb7d'
// Look up the named view by name and then rewrite it with the same uuid each time
const nameToUuid: Map<string, string> = new Map()
nameToUuid.set('uuid1', uuid1)
nameToUuid.set('uuid2', uuid2)
nameToUuid.set('uuid3', uuid3)
/**
* Given the project.toml string, overwrite the named views to be the constant uuid
* values to match the snapshots. The uuids are randomly generated
*/
function tomlStringOverWriteNamedViewUuids(toml: string): string {
const settings = tomlToPerProjectSettings(toml)
const namedViews = settings.settings?.app?.named_views
if (namedViews) {
const entries = Object.entries(namedViews)
const remappedNamedViews: { [key: string]: NamedView } = {}
entries.forEach(([_, value]) => {
if (value) {
// {name:'uuid1'} -> uuid1 lookup
const staticUuid = nameToUuid.get(value.name)
if (staticUuid) {
remappedNamedViews[staticUuid] = value
}
}
})
if (settings && settings.settings && settings.settings.app) {
settings.settings.app.named_views = remappedNamedViews
}
}
return perProjectsettingsToToml(settings)
}
test.describe('Named view tests', () => {
test('Verify project.toml is not created', async ({ page }, testInfo) => {
// Create project and load it
const projectName = 'named-views'
await createProject({ name: projectName, page })
// Generate file paths for project.toml
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
const tempProjectSettingsFilePath = join(
projectDirName,
projectName,
PROJECT_SETTINGS_FILE_NAME
)
// project.toml should not exist on initial project creation
let exists = await fileExists(tempProjectSettingsFilePath)
expect(exists).toBe(false)
})
test('Verify named view gets created', async ({
cmdBar,
scene,
page,
}, testInfo) => {
const projectName = 'named-views'
const myNamedView = 'uuid1'
// Create and load project
await createProject({ name: projectName, page })
await scene.waitForExecutionDone()
// Create named view
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create named view')
await cmdBar.argumentInput.fill(myNamedView)
await cmdBar.progressCmdBar(false)
// Generate paths for the project.toml
const tempProjectSettingsFilePath = join(
projectDirName,
projectName,
PROJECT_SETTINGS_FILE_NAME
)
// Expect project.toml to be generated on disk since a named view was created
await expect(async () => {
let exists = await fileExists(tempProjectSettingsFilePath)
expect(exists).toBe(true)
}).toPass()
// Read project.toml into memory
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
// Write the entire tomlString to a snapshot.
// There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created')
})
test('Verify named view gets deleted', async ({
cmdBar,
scene,
page,
}, testInfo) => {
const projectName = 'named-views'
const myNamedView1 = 'uuid1'
const myNamedView2 = 'uuid2'
// Create project and go into the project
await createProject({ name: projectName, page })
await scene.waitForExecutionDone()
// Create a new named view
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create named view')
await cmdBar.argumentInput.fill(myNamedView1)
await cmdBar.progressCmdBar(false)
// Generate file paths for project.toml
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
const tempProjectSettingsFilePath = join(
projectDirName,
projectName,
PROJECT_SETTINGS_FILE_NAME
)
// Except the project.toml to be written to disk since a named view was created
await expect(async () => {
let exists = await fileExists(tempProjectSettingsFilePath)
expect(exists).toBe(true)
}).toPass()
// Read project.toml into memory
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
// Write the entire tomlString to a snapshot.
// There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created')
// Delete a named view
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('delete named view')
cmdBar.selectOption({ name: myNamedView2 })
await cmdBar.progressCmdBar(false)
// Read project.toml into memory again since we deleted a named view
tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
// // Write the entire tomlString to a snapshot.
// // There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-named-view-gets-deleted')
})
test('Verify named view gets loaded', async ({
cmdBar,
scene,
page,
}, testInfo) => {
const projectName = 'named-views'
const myNamedView = 'uuid1'
// Create project and go into the project
await createProject({ name: projectName, page })
await scene.waitForExecutionDone()
// Create a new named view
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create named view')
await cmdBar.argumentInput.fill(myNamedView)
await cmdBar.progressCmdBar(false)
// Generate file paths for project.toml
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
const tempProjectSettingsFilePath = join(
projectDirName,
projectName,
PROJECT_SETTINGS_FILE_NAME
)
// Except the project.toml to be written to disk since a named view was created
await expect(async () => {
let exists = await fileExists(tempProjectSettingsFilePath)
expect(exists).toBe(true)
}).toPass()
// Read project.toml into memory
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
// Write the entire tomlString to a snapshot.
// There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created')
// Create a load a named view
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('load named view')
await cmdBar.argumentInput.fill(myNamedView)
await cmdBar.progressCmdBar(false)
// Check the toast appeared
await expect(
page.getByText(`Named view ${myNamedView} loaded.`)
).toBeVisible()
})
test('Verify two named views get created', async ({
cmdBar,
scene,
page,
}, testInfo) => {
const projectName = 'named-views'
const myNamedView1 = 'uuid1'
const myNamedView2 = 'uuid2'
// Create and load project
await createProject({ name: projectName, page })
await scene.waitForExecutionDone()
// Create named view
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create named view')
await cmdBar.argumentInput.fill(myNamedView1)
await cmdBar.progressCmdBar(false)
await page.waitForTimeout(1000)
const orbitMouseStart = { x: 800, y: 130 }
const orbitMouseEnd = { x: 0, y: 130 }
await page.mouse.move(orbitMouseStart.x, orbitMouseStart.y)
await page.mouse.down({ button: 'middle' })
await page.mouse.move(orbitMouseEnd.x, orbitMouseEnd.y, {
steps: 3,
})
await page.mouse.up({ button: 'middle' })
await page.waitForTimeout(1000)
await cmdBar.openCmdBar()
await cmdBar.chooseCommand('create named view')
await cmdBar.argumentInput.fill(myNamedView2)
await cmdBar.progressCmdBar(false)
// Wait a moment for the project.toml to get written to disk with the new view point
await page.waitForTimeout(1000)
// Generate paths for the project.toml
const tempProjectSettingsFilePath = join(
projectDirName,
projectName,
PROJECT_SETTINGS_FILE_NAME
)
// Expect project.toml to be generated on disk since a named view was created
await expect(async () => {
let exists = await fileExists(tempProjectSettingsFilePath)
expect(exists).toBe(true)
}).toPass()
// Read project.toml into memory
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
// Write the entire tomlString to a snapshot.
// There are many key/value pairs to check this is a safer match.
expect(tomlString).toMatchSnapshot('verify-two-named-view-gets-created')
})
})

View File

@ -0,0 +1,16 @@
[settings]
modeling = { }
text_editor = { }
command_bar = { }
[settings.app.named_views.0656fb1a-9640-473e-b334-591dc70c0138]
name = "uuid1"
eye_offset = 1_378.0059
fov_y = 45
is_ortho = false
ortho_scale_enabled = true
ortho_scale_factor = 1.6
pivot_position = [ 0, 0, 0 ]
pivot_rotation = [ 0.5380994, 0.0, 0.0, 0.8428814 ]
world_coord_system = "right_handed_up_z"
version = 1

View File

@ -0,0 +1,16 @@
[settings]
modeling = { }
text_editor = { }
command_bar = { }
[settings.app.named_views.0656fb1a-9640-473e-b334-591dc70c0138]
name = "uuid1"
eye_offset = 1_378.0059
fov_y = 45
is_ortho = false
ortho_scale_enabled = true
ortho_scale_factor = 1.6
pivot_position = [ 0, 0, 0 ]
pivot_rotation = [ 0.5380994, 0.0, 0.0, 0.8428814 ]
world_coord_system = "right_handed_up_z"
version = 1

View File

@ -0,0 +1,28 @@
[settings]
modeling = { }
text_editor = { }
command_bar = { }
[settings.app.named_views.0656fb1a-9640-473e-b334-591dc70c0138]
name = "uuid1"
eye_offset = 1_378.0059
fov_y = 45
is_ortho = false
ortho_scale_enabled = true
ortho_scale_factor = 1.6
pivot_position = [ 0, 0, 0 ]
pivot_rotation = [ 0.5380994, 0.0, 0.0, 0.8428814 ]
world_coord_system = "right_handed_up_z"
version = 1
[settings.app.named_views.c810cf04-c6cc-4a4a-8b11-17bf445dcab7]
name = "uuid2"
eye_offset = 1_378.0059
fov_y = 45
is_ortho = false
ortho_scale_enabled = true
ortho_scale_factor = 1.6
pivot_position = [ 1_826.5239, 0.0, 0.0 ]
pivot_rotation = [ 0.5380994, 0.0, 0.0, 0.8428814 ]
world_coord_system = "right_handed_up_z"
version = 1

View File

@ -26,6 +26,7 @@ import { isArray } from 'lib/utils'
import { reportRejection } from 'lib/trap'
import { DeepPartial } from 'lib/types'
import { Configuration } from 'lang/wasm'
import { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'
const toNormalizedCode = (text: string) => {
return text.replace(/\s+/g, '')
@ -761,7 +762,7 @@ export interface Paths {
}
export const doExport = async (
output: Models['OutputFormat_type'],
output: Models['OutputFormat3d_type'],
rootDir: string,
page: Page,
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
@ -1125,3 +1126,15 @@ export function settingsToToml(settings: DeepPartial<Configuration>) {
export function tomlToSettings(toml: string): DeepPartial<Configuration> {
return TOML.parse(toml)
}
export function tomlToPerProjectSettings(
toml: string
): DeepPartial<ProjectConfiguration> {
return TOML.parse(toml)
}
export function perProjectsettingsToToml(
settings: DeepPartial<ProjectConfiguration>
) {
return TOML.stringify(settings as any)
}

View File

@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "2.0.17",
"@kittycad/lib": "2.0.21",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1",

View File

@ -158,13 +158,13 @@ fn named_view_point_version_one() -> f64 {
#[ts(export)]
pub struct NamedView {
/// User defined name to identify the named view. A label.
#[serde(default, alias = "name", skip_serializing_if = "is_default")]
#[serde(default, alias = "name")]
pub name: String,
/// Engine camera eye off set
#[serde(default, alias = "eyeOffset", skip_serializing_if = "is_default")]
#[serde(default, alias = "eyeOffset")]
pub eye_offset: f64,
/// Engine camera vertical FOV
#[serde(default, alias = "fovY", skip_serializing_if = "is_default")]
#[serde(default, alias = "fovY")]
pub fov_y: f64,
// Engine camera is orthographic or perspective projection
#[serde(default, alias = "isOrtho")]
@ -173,16 +173,16 @@ pub struct NamedView {
#[serde(default, alias = "orthoScaleEnabled")]
pub ortho_scale_enabled: bool,
/// Engine camera orthographic scaling factor
#[serde(default, alias = "orthoScaleFactor", skip_serializing_if = "is_default")]
#[serde(default, alias = "orthoScaleFactor")]
pub ortho_scale_factor: f64,
/// Engine camera position that the camera pivots around
#[serde(default, alias = "pivotPosition", skip_serializing_if = "is_default")]
#[serde(default, alias = "pivotPosition")]
pub pivot_position: [f64; 3],
/// Engine camera orientation in relation to the pivot position
#[serde(default, alias = "pivotRotation", skip_serializing_if = "is_default")]
#[serde(default, alias = "pivotRotation")]
pub pivot_rotation: [f64; 4],
/// Engine camera world coordinate system orientation
#[serde(default, alias = "worldCoordSystem", skip_serializing_if = "is_default")]
#[serde(default, alias = "worldCoordSystem")]
pub world_coord_system: String,
/// Version number of the view point if the engine camera API changes
#[serde(default = "named_view_point_version_one")]

View File

@ -4,7 +4,6 @@ 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,
@ -61,8 +60,6 @@ export const FileMachineProvider = ({
)
useEffect(() => {
// TODO: Engine feature is not deployed
if (DEV) {
const {
createNamedViewCommand,
deleteNamedViewCommand,
@ -89,7 +86,6 @@ export const FileMachineProvider = ({
},
})
}
}
}, [])
// Due to the route provider, i've moved this to the FileMachineProvider instead of CommandBarProvider

View File

@ -24,7 +24,7 @@ import { getVariableDeclaration } from 'lang/queryAst/getVariableDeclaration'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { getNodeFromPath } from 'lang/queryAst'
type OutputFormat = Models['OutputFormat_type']
type OutputFormat = Models['OutputFormat3d_type']
type OutputTypeKey = OutputFormat['type']
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
type StorageUnion = ExtractStorageTypes<OutputFormat>

View File

@ -3,8 +3,88 @@ 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'
import { settingsActor, getSettings } from 'machines/appMachine'
import { err, reportRejection } from 'lib/trap'
import {
CameraViewState_type,
WorldCoordinateSystem_type,
} from '@kittycad/lib/dist/types/src/models'
function isWorldCoordinateSystemType(
x: string
): x is WorldCoordinateSystem_type {
return x === 'right_handed_up_z' || x === 'right_handed_up_y'
}
function namedViewToCameraViewState(
namedView: NamedView
): CameraViewState_type | Error {
const worldCoordinateSystem: string = namedView.world_coord_system
if (!isWorldCoordinateSystemType(worldCoordinateSystem)) {
return new Error('world coordinate system is not typed')
}
const cameraViewState: CameraViewState_type = {
eye_offset: namedView.eye_offset,
fov_y: namedView.fov_y,
ortho_scale_enabled: namedView.ortho_scale_enabled,
ortho_scale_factor: namedView.ortho_scale_factor,
world_coord_system: worldCoordinateSystem,
is_ortho: namedView.is_ortho,
pivot_position: namedView.pivot_position,
pivot_rotation: namedView.pivot_rotation,
}
return cameraViewState
}
function cameraViewStateToNamedView(
name: string,
cameraViewState: CameraViewState_type
): NamedView | Error {
let pivot_position: [number, number, number] | null = null
let pivot_rotation: [number, number, number, number] | null = null
if (cameraViewState.pivot_position.length === 3) {
pivot_position = [
cameraViewState.pivot_position[0],
cameraViewState.pivot_position[1],
cameraViewState.pivot_position[2],
]
} else {
return new Error(`invalid pivot position ${cameraViewState.pivot_position}`)
}
if (cameraViewState.pivot_rotation.length === 4) {
pivot_rotation = [
cameraViewState.pivot_rotation[0],
cameraViewState.pivot_rotation[1],
cameraViewState.pivot_rotation[2],
cameraViewState.pivot_rotation[3],
]
} else {
return new Error(`invalid pivot rotation ${cameraViewState.pivot_rotation}`)
}
// Create a new named view
const requestedView: NamedView = {
name,
eye_offset: cameraViewState.eye_offset,
fov_y: cameraViewState.fov_y,
ortho_scale_enabled: cameraViewState.ortho_scale_enabled,
ortho_scale_factor: cameraViewState.ortho_scale_factor,
world_coord_system: cameraViewState.world_coord_system,
is_ortho: cameraViewState.is_ortho,
pivot_position,
pivot_rotation,
// TS side knows about the version for the time being the version is not used for anything for now.
// Can be detected and cleaned up later if we have new version.
version: 1.0,
}
return requestedView
}
export function createNamedViewsCommand() {
// Creates a command to be registered in the command bar.
@ -30,7 +110,6 @@ export function createNamedViewsCommand() {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
// @ts-ignore TODO: Not in production yet.
cmd: { type: 'default_camera_get_view' },
})
@ -39,12 +118,20 @@ export function createNamedViewsCommand() {
}
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,
if (cameraGetViewResponse.success) {
if (
cameraGetViewResponse.resp.data.modeling_response.type ===
'default_camera_get_view'
) {
const view =
cameraGetViewResponse.resp.data.modeling_response.data
const requestedView = cameraViewStateToNamedView(
data.name,
view.view
)
if (err(requestedView)) {
toast.error('Unable to create named view.')
return
}
// Retrieve application state for namedViews
const namedViews = {
@ -64,9 +151,12 @@ export function createNamedViewsCommand() {
value: requestedNamedViews,
},
})
toast.success(`Named view ${requestedView.name} created.`)
}
}
}
}
invokeAndForgetCreateNamedView().catch(reportRejection)
},
args: {
@ -120,9 +210,10 @@ export function createNamedViewsCommand() {
name: {
required: true,
inputType: 'options',
options: () => {
options: (commandBar, machineContext) => {
const settings = getSettings()
const namedViews = {
...settingsActor.getSnapshot().context.app.namedViews.current,
...settings.app.namedViews.current,
}
const options: CommandArgumentOption<any>[] = []
Object.entries(namedViews).forEach(([key, view]) => {
@ -164,6 +255,12 @@ export function createNamedViewsCommand() {
if (viewToLoad) {
// Split into the name and the engine data
const { name, version, ...engineViewData } = viewToLoad
const cameraViewState = namedViewToCameraViewState(viewToLoad)
if (err(cameraViewState)) {
toast.error(`Unable to load named view ${data.name}`)
return
}
// Only send the specific camera information, the NamedView itself
// is not directly compatible with the engine API
@ -171,10 +268,9 @@ export function createNamedViewsCommand() {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
// @ts-ignore TODO: Not in production yet.
type: 'default_camera_set_view',
view: {
...engineViewData,
...cameraViewState,
},
},
})
@ -190,6 +286,16 @@ export function createNamedViewsCommand() {
},
})
// Update the camera by triggering the callback workflow to get the camera settings
// Setting the view won't update the client side camera.
// Asking for the default camera settings after setting the view will internally sync the camera
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
toast.success(`Named view ${name} loaded.`)
} else {
toast.error(`Unable to load named view, could not find named view`)
@ -202,8 +308,9 @@ export function createNamedViewsCommand() {
required: true,
inputType: 'options',
options: () => {
const settings = getSettings()
const namedViews = {
...settingsActor.getSnapshot().context.app.namedViews.current,
...settings.app.namedViews.current,
}
const options: CommandArgumentOption<any>[] = []
Object.entries(namedViews).forEach(([key, view]) => {

View File

@ -89,7 +89,7 @@ export function isNamedView(
] as const
return namedViewKeys.every((key) => {
return namedView && namedView[key]
return namedView && key in namedView
})
}

View File

@ -1844,10 +1844,10 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@kittycad/lib@2.0.17":
version "2.0.17"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-2.0.17.tgz#26e74f8f6ef534d1987161dfccf62ac03d3b3ed1"
integrity sha512-W4YcGvLfbeA2drjmAHDe1x6v2OXbvhB7nqtdjtKtPK39pGtMC41btcTx5MNvfz7pdnd32MIM02TCRqN6YV0RMw==
"@kittycad/lib@2.0.21":
version "2.0.21"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-2.0.21.tgz#b5ccb03367f4478896e5ef14221a8512d15ea4d4"
integrity sha512-JK2lAJm22GEVKX1Q57M2Pbnqzt8vmaXHec/9MDGIodnzWB36QEEs4VVVTIlJNbjoYoa+au5feakjTXUiuLu2cg==
dependencies:
openapi-types "^12.0.0"
ts-node "^10.9.1"