Files
modeling-app/src/lang/wasm.ts
Nick Cameron 322f398049 ProgramMemory refactor - eliminate copies of memory (#5273)
* refactor program memory for encapsulation and remove special treatment of return values

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor ProgramMemory to isolate mutation

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor ProgramMemory

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Generated output

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Cache the result of executing modules for their items

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Compress envs when popped

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Remove the last traces of geometry from the memory module

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* docs

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fixups

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Frontend

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Improve Environment GC

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Fixup mock execution frontend and interpreter, misc bug fixes

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-11 21:22:56 +00:00

773 lines
23 KiB
TypeScript

import {
init,
parse_wasm,
recast_wasm,
format_number,
execute_with_engine,
execute_mock,
kcl_lint,
modify_ast_for_sketch_wasm,
is_points_ccw,
get_tangential_arc_to_info,
program_memory_init,
make_default_planes,
coredump,
toml_stringify,
default_app_settings,
parse_app_settings,
parse_project_settings,
default_project_settings,
base64_decode,
clear_scene_and_bust_cache,
kcl_settings,
change_kcl_settings,
reloadModule,
} from 'lib/wasm_lib_wrapper'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { EngineCommandManager } from './std/engineConnection'
import { Discovered } from '../wasm-lib/kcl/bindings/Discovered'
import { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
import type { Program } from '../wasm-lib/kcl/bindings/Program'
import { Coords2d } from './std/sketch'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import { CoreDumpInfo } from 'wasm-lib/kcl/bindings/CoreDumpInfo'
import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env'
import { err, Reason } from 'lib/trap'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { DeepPartial } from 'lib/types'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Sketch } from '../wasm-lib/kcl/bindings/Sketch'
import { ExecOutcome as RustExecOutcome } from 'wasm-lib/kcl/bindings/ExecOutcome'
import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory'
import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
import { Environment } from '../wasm-lib/kcl/bindings/Environment'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
import { Artifact as RustArtifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact'
import { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
import { UnitAngle, UnitLength } from 'wasm-lib/kcl/bindings/ModelingCmd'
import { UnitLen } from 'wasm-lib/kcl/bindings/UnitLen'
import { UnitAngle as UnitAng } from 'wasm-lib/kcl/bindings/UnitAngle'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactId } from 'wasm-lib/kcl/bindings/Artifact'
export type { Cap as CapArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { CodeRef } from 'wasm-lib/kcl/bindings/Artifact'
export type { EdgeCut } from 'wasm-lib/kcl/bindings/Artifact'
export type { Path as PathArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Plane as PlaneArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Segment as SegmentArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Solid2d as Solid2dArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Sweep as SweepArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { SweepEdge } from 'wasm-lib/kcl/bindings/Artifact'
export type { Wall as WallArtifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
export type { ObjectProperty } from '../wasm-lib/kcl/bindings/ObjectProperty'
export type { MemberExpression } from '../wasm-lib/kcl/bindings/MemberExpression'
export type { PipeExpression } from '../wasm-lib/kcl/bindings/PipeExpression'
export type { VariableDeclaration } from '../wasm-lib/kcl/bindings/VariableDeclaration'
export type { Parameter } from '../wasm-lib/kcl/bindings/Parameter'
export type { PipeSubstitution } from '../wasm-lib/kcl/bindings/PipeSubstitution'
export type { Identifier } from '../wasm-lib/kcl/bindings/Identifier'
export type { UnaryExpression } from '../wasm-lib/kcl/bindings/UnaryExpression'
export type { BinaryExpression } from '../wasm-lib/kcl/bindings/BinaryExpression'
export type { ReturnStatement } from '../wasm-lib/kcl/bindings/ReturnStatement'
export type { ExpressionStatement } from '../wasm-lib/kcl/bindings/ExpressionStatement'
export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression'
export type { CallExpressionKw } from '../wasm-lib/kcl/bindings/CallExpressionKw'
export type { LabeledArg } from '../wasm-lib/kcl/bindings/LabeledArg'
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
export type { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
export type SyntaxType =
| 'Program'
| 'ExpressionStatement'
| 'BinaryExpression'
| 'CallExpression'
| 'CallExpressionKw'
| 'Identifier'
| 'ReturnStatement'
| 'VariableDeclaration'
| 'VariableDeclarator'
| 'MemberExpression'
| 'ArrayExpression'
| 'ObjectExpression'
| 'ObjectProperty'
| 'FunctionExpression'
| 'PipeExpression'
| 'PipeSubstitution'
| 'Literal'
| 'LiteralValue'
| 'NonCodeNode'
| 'UnaryExpression'
export type { Path } from '../wasm-lib/kcl/bindings/Path'
export type { Sketch } from '../wasm-lib/kcl/bindings/Sketch'
export type { Solid } from '../wasm-lib/kcl/bindings/Solid'
export type { KclValue } from '../wasm-lib/kcl/bindings/KclValue'
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
/**
* Convert a SourceRange as used inside the KCL interpreter into the above one for use in the
* frontend (essentially we're eagerly checking whether the frontend should care about the SourceRange
* so as not to expose details of the interpreter's current representation of module ids throughout
* the frontend).
*/
export function sourceRangeFromRust(s: SourceRange): SourceRange {
return [s[0], s[1], s[2]]
}
/**
* Create a default SourceRange for testing or as a placeholder.
*/
export function defaultSourceRange(): SourceRange {
return [0, 0, 0]
}
/**
* Create a SourceRange for the top-level module.
*/
export function topLevelRange(start: number, end: number): SourceRange {
return [start, end, 0]
}
/**
* Returns true if this source range is from the file being executed. Returns
* false if it's from a file that was imported.
*/
export function isTopLevelModule(range: SourceRange): boolean {
return range[2] === 0
}
function firstSourceRange(error: RustKclError): SourceRange {
return error.sourceRanges.length > 0
? sourceRangeFromRust(error.sourceRanges[0])
: defaultSourceRange()
}
export const wasmUrl = () => {
// For when we're in electron (file based) or web server (network based)
// For some reason relative paths don't work as expected. Otherwise we would
// just do /wasm_lib_bg.wasm. In particular, the issue arises when the path
// is used from within worker.ts.
const fullUrl = document.location.protocol.includes('http')
? document.location.origin + '/wasm_lib_bg.wasm'
: document.location.protocol +
document.location.pathname.split('/').slice(0, -1).join('/') +
'/wasm_lib_bg.wasm'
return fullUrl
}
// Initialise the wasm module.
const initialise = async () => {
try {
await reloadModule()
const fullUrl = wasmUrl()
const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer()
return await init({ module_or_path: buffer })
} catch (e) {
console.log('Error initialising WASM', e)
return Promise.reject(e)
}
}
export const initPromise = initialise()
const splitErrors = (
input: CompilationError[]
): { errors: CompilationError[]; warnings: CompilationError[] } => {
let errors = []
let warnings = []
for (const i of input) {
if (i.severity === 'Warning') {
warnings.push(i)
} else {
errors.push(i)
}
}
return { errors, warnings }
}
export class ParseResult {
program: Node<Program> | null
errors: CompilationError[]
warnings: CompilationError[]
constructor(
program: Node<Program> | null,
errors: CompilationError[],
warnings: CompilationError[]
) {
this.program = program
this.errors = errors
this.warnings = warnings
}
}
/**
* Parsing was successful. There is guaranteed to be an AST and no fatal errors. There may or may
* not be warnings or non-fatal errors.
*/
class SuccessParseResult extends ParseResult {
program: Node<Program>
constructor(
program: Node<Program>,
errors: CompilationError[],
warnings: CompilationError[]
) {
super(program, errors, warnings)
this.program = program
}
}
export function resultIsOk(result: ParseResult): result is SuccessParseResult {
return !!result.program && result.errors.length === 0
}
export const parse = (code: string | Error): ParseResult | Error => {
if (err(code)) return code
try {
const parsed: [Node<Program>, CompilationError[]] = parse_wasm(code)
let errs = splitErrors(parsed[1])
return new ParseResult(parsed[0], errs.errors, errs.warnings)
} catch (e: any) {
// throw e
console.error(e.toString())
const parsed: RustKclError = JSON.parse(e.toString())
return new KCLError(
parsed.kind,
parsed.msg,
firstSourceRange(parsed),
[],
[],
defaultArtifactGraph()
)
}
}
// Parse and throw an exception if there are any errors (probably not suitable for use outside of testing).
export const assertParse = (code: string): Node<Program> => {
const result = parse(code)
// eslint-disable-next-line suggest-no-throw/suggest-no-throw
if (err(result) || !resultIsOk(result)) throw result
return result.program
}
export type VariableMap = { [key in string]?: KclValue }
export type PathToNode = [string | number, string][]
export const isPathToNodeNumber = (
pathToNode: string | number
): pathToNode is number => {
return typeof pathToNode === 'number'
}
export interface ExecState {
variables: { [key in string]?: KclValue }
operations: Operation[]
artifacts: { [key in ArtifactId]?: RustArtifact }
artifactCommands: ArtifactCommand[]
artifactGraph: ArtifactGraph
}
/**
* Create an empty ExecState. This is useful on init to prevent needing an
* Option.
*/
export function emptyExecState(): ExecState {
return {
variables: {},
operations: [],
artifacts: {},
artifactCommands: [],
artifactGraph: defaultArtifactGraph(),
}
}
function execStateFromRust(
execOutcome: RustExecOutcome,
program: Node<Program>
): ExecState {
const artifactGraph = rustArtifactGraphToMap(execOutcome.artifactGraph)
// We haven't ported pathToNode logic to Rust yet, so we need to fill it in.
for (const [id, artifact] of artifactGraph) {
if (!artifact) continue
if (!('codeRef' in artifact)) continue
const pathToNode = getNodePathFromSourceRange(
program,
sourceRangeFromRust(artifact.codeRef.range)
)
artifact.codeRef.pathToNode = pathToNode
}
return {
variables: execOutcome.variables,
operations: execOutcome.operations,
artifacts: execOutcome.artifacts,
artifactCommands: execOutcome.artifactCommands,
artifactGraph,
}
}
function mockExecStateFromRust(execOutcome: RustExecOutcome): ExecState {
return {
variables: execOutcome.variables,
operations: execOutcome.operations,
artifacts: execOutcome.artifacts,
artifactCommands: execOutcome.artifactCommands,
artifactGraph: new Map<ArtifactId, Artifact>(),
}
}
export type ArtifactGraph = Map<ArtifactId, Artifact>
function rustArtifactGraphToMap(
rustArtifactGraph: RustArtifactGraph
): ArtifactGraph {
const map = new Map<ArtifactId, Artifact>()
for (const [id, artifact] of Object.entries(rustArtifactGraph.map)) {
if (!artifact) continue
map.set(id, artifact)
}
return map
}
export function defaultArtifactGraph(): ArtifactGraph {
return new Map()
}
// TODO: In the future, make the parameter be a KclValue.
export function sketchFromKclValueOptional(
obj: any,
varName: string | null
): Sketch | Reason {
if (obj?.value?.type === 'Sketch') return obj.value
if (obj?.value?.type === 'Solid') return obj.value.sketch
if (obj?.type === 'Sketch') return obj.value
if (obj?.type === 'Solid') return obj.value.sketch
if (!varName) {
varName = 'a KCL value'
}
const actualType = obj?.value?.type ?? obj?.type
if (actualType) {
return new Reason(
`Expected ${varName} to be a sketch or solid, but it was ${actualType} instead.`
)
} else {
return new Reason(`Expected ${varName} to be a sketch, but it wasn't.`)
}
}
// TODO: In the future, make the parameter be a KclValue.
export function sketchFromKclValue(
obj: any,
varName: string | null
): Sketch | Error {
const result = sketchFromKclValueOptional(obj, varName)
if (result instanceof Reason) {
return result.toError()
}
return result
}
/**
* Execute a KCL program.
* @param node The AST of the program to execute.
* @param path The full path of the file being executed. Use `null` for
* expressions that don't have a file, like expressions in the command bar.
*/
export const executeMock = async (
node: Node<Program>,
usePrevMemory?: boolean,
path?: string,
variables?: { [key in string]?: KclValue }
): Promise<ExecState> => {
try {
if (!variables) {
variables = {}
}
if (usePrevMemory === undefined) {
usePrevMemory = true
}
const execOutcome: RustExecOutcome = await execute_mock(
JSON.stringify(node),
path,
JSON.stringify({ settings: await jsAppSettings() }),
usePrevMemory,
JSON.stringify(variables),
fileSystemManager
)
return mockExecStateFromRust(execOutcome)
} catch (e: any) {
return Promise.reject(errFromErrWithOutputs(e))
}
}
/**
* Execute a KCL program.
* @param node The AST of the program to execute.
* @param path The full path of the file being executed. Use `null` for
* expressions that don't have a file, like expressions in the command bar.
*/
export const executeWithEngine = async (
node: Node<Program>,
engineCommandManager: EngineCommandManager,
path?: string
): Promise<ExecState> => {
try {
const execOutcome: RustExecOutcome = await execute_with_engine(
JSON.stringify(node),
path,
JSON.stringify({ settings: await jsAppSettings() }),
engineCommandManager,
fileSystemManager
)
return execStateFromRust(execOutcome, node)
} catch (e: any) {
return Promise.reject(errFromErrWithOutputs(e))
}
}
const jsAppSettings = async () => {
let jsAppSettings = default_app_settings()
if (!TEST) {
const lastSettingsSnapshot = await import(
'components/SettingsAuthProvider'
).then((module) => module.lastSettingsContextSnapshot)
if (lastSettingsSnapshot) {
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
}
}
return jsAppSettings
}
const errFromErrWithOutputs = (e: any): KCLError => {
console.log('execute error', e)
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
return new KCLError(
parsed.error.kind,
parsed.error.msg,
firstSourceRange(parsed.error),
parsed.operations,
parsed.artifactCommands,
rustArtifactGraphToMap(parsed.artifactGraph)
)
}
export const kclLint = async (ast: Program): Promise<Array<Discovered>> => {
try {
const discovered_findings: Array<Discovered> = await kcl_lint(
JSON.stringify(ast)
)
return discovered_findings
} catch (e: any) {
return Promise.reject(e)
}
}
export const recast = (ast: Program): string | Error => {
return recast_wasm(JSON.stringify(ast))
}
/**
* Format a number with suffix as KCL.
*/
export function formatNumber(value: number, suffix: NumericSuffix): string {
return format_number(value, JSON.stringify(suffix))
}
export const makeDefaultPlanes = async (
engineCommandManager: EngineCommandManager
): Promise<DefaultPlanes> => {
try {
const planes: DefaultPlanes = await make_default_planes(
engineCommandManager
)
return planes
} catch (e) {
// TODO: do something real with the error.
console.log('make default planes error', e)
return Promise.reject(e)
}
}
export const modifyAstForSketch = async (
engineCommandManager: EngineCommandManager,
ast: Node<Program>,
variableName: string,
currentPlane: string,
engineId: string
): Promise<Node<Program>> => {
try {
const updatedAst: Node<Program> = await modify_ast_for_sketch_wasm(
engineCommandManager,
JSON.stringify(ast),
variableName,
JSON.stringify(currentPlane),
engineId
)
return updatedAst
} catch (e: any) {
const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError(
parsed.kind,
parsed.msg,
firstSourceRange(parsed),
[],
[],
defaultArtifactGraph()
)
return Promise.reject(kclError)
}
}
export function isPointsCCW(points: Coords2d[]): number {
return is_points_ccw(new Float64Array(points.flat()))
}
export function getTangentialArcToInfo({
arcStartPoint,
arcEndPoint,
tanPreviousPoint,
obtuse = true,
}: {
arcStartPoint: Coords2d
arcEndPoint: Coords2d
tanPreviousPoint: Coords2d
obtuse?: boolean
}): {
center: Coords2d
arcMidPoint: Coords2d
radius: number
startAngle: number
endAngle: number
ccw: boolean
arcLength: number
} {
const result = get_tangential_arc_to_info(
arcStartPoint[0],
arcStartPoint[1],
arcEndPoint[0],
arcEndPoint[1],
tanPreviousPoint[0],
tanPreviousPoint[1],
obtuse
)
return {
center: [result.center_x, result.center_y],
arcMidPoint: [result.arc_mid_point_x, result.arc_mid_point_y],
radius: result.radius,
startAngle: result.start_angle,
endAngle: result.end_angle,
ccw: result.ccw > 0,
arcLength: result.arc_length,
}
}
export async function coreDump(
coreDumpManager: CoreDumpManager,
openGithubIssue: boolean = false
): Promise<CoreDumpInfo> {
try {
console.warn('CoreDump: Initializing core dump')
const dump: CoreDumpInfo = await coredump(coreDumpManager)
/* NOTE: this console output of the coredump should include the field
`github_issue_url` which is not in the uploaded coredump file.
`github_issue_url` is added after the file is uploaded
and is only needed for the openWindow operation which creates
a new GitHub issue for the user.
*/
if (openGithubIssue && dump.github_issue_url) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
openWindow(dump.github_issue_url)
} else {
console.error(
'github_issue_url undefined. Unable to create GitHub issue for coredump.'
)
}
console.log('CoreDump: final coredump', dump)
console.log('CoreDump: final coredump JSON', JSON.stringify(dump))
return dump
} catch (e: any) {
console.error('CoreDump: error', e)
return Promise.reject(new Error(`Error getting core dump: ${e}`))
}
}
export function tomlStringify(toml: any): string | Error {
return toml_stringify(JSON.stringify(toml))
}
export function defaultAppSettings(): DeepPartial<Configuration> | Error {
return default_app_settings()
}
export async function clearSceneAndBustCache(
engineCommandManager: EngineCommandManager
): Promise<null | Error> {
try {
await clear_scene_and_bust_cache(engineCommandManager)
} catch (e: any) {
console.error('clear_scene_and_bust_cache: error', e)
return Promise.reject(
new Error(`Error on clear_scene_and_bust_cache: ${e}`)
)
}
return null
}
export function parseAppSettings(
toml: string
): DeepPartial<Configuration> | Error {
return parse_app_settings(toml)
}
export function defaultProjectSettings():
| DeepPartial<ProjectConfiguration>
| Error {
return default_project_settings()
}
export function parseProjectSettings(
toml: string
): DeepPartial<ProjectConfiguration> | Error {
return parse_project_settings(toml)
}
export function base64Decode(base64: string): ArrayBuffer | Error {
try {
const decoded = base64_decode(base64)
return new Uint8Array(decoded).buffer
} catch (e) {
console.error('Caught error decoding base64 string: ' + e)
return new Error('Caught error decoding base64 string: ' + e)
}
}
/**
* Get the meta settings for the KCL. If no settings were set in the file,
* returns null.
*/
export function kclSettings(
kcl: string | Node<Program>
): MetaSettings | null | Error {
let program: Node<Program>
if (typeof kcl === 'string') {
const parseResult = parse(kcl)
if (err(parseResult)) return parseResult
if (!resultIsOk(parseResult)) {
return new Error(`parse result had errors`, { cause: parseResult })
}
program = parseResult.program
} else {
program = kcl
}
try {
return kcl_settings(JSON.stringify(program))
} catch (e) {
return new Error('Caught error getting kcl settings', { cause: e })
}
}
/**
* Change the meta settings for the kcl file.
* @returns the new kcl string with the updated settings.
*/
export function changeKclSettings(
kcl: string,
settings: MetaSettings
): string | Error {
try {
return change_kcl_settings(kcl, JSON.stringify(settings))
} catch (e) {
console.error('Caught error changing kcl settings', e)
return new Error('Caught error changing kcl settings', { cause: e })
}
}
/**
* Convert a `UnitLength_type` to a `UnitLen`
*/
export function unitLengthToUnitLen(input: UnitLength): UnitLen {
switch (input) {
case 'm':
return { type: 'M' }
case 'cm':
return { type: 'Cm' }
case 'yd':
return { type: 'Yards' }
case 'ft':
return { type: 'Feet' }
case 'in':
return { type: 'Inches' }
default:
return { type: 'Mm' }
}
}
/**
* Convert `UnitLen` to `UnitLength_type`.
*/
export function unitLenToUnitLength(input: UnitLen): UnitLength {
switch (input.type) {
case 'M':
return 'm'
case 'Cm':
return 'cm'
case 'Yards':
return 'yd'
case 'Feet':
return 'ft'
case 'Inches':
return 'in'
default:
return 'mm'
}
}
/**
* Convert `UnitAngle` to `UnitAngle_type`.
*/
export function unitAngToUnitAngle(input: UnitAng): UnitAngle {
switch (input.type) {
case 'Radians':
return 'radians'
default:
return 'degrees'
}
}