Files
modeling-app/src/lib/commandBarConfigs/validators.ts

318 lines
8.9 KiB
TypeScript

import type { Models } from '@kittycad/lib'
import type { Selections } from '@src/lib/selections'
import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { isArray, uuidv4 } from '@src/lib/utils'
import type { CommandBarContext } from '@src/machines/commandBarMachine'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) {
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'disable_dry_run' },
})
// Exit out since the command was successful
return
} catch (e) {
console.error(e)
console.error('disable_dry_run failed. This is bad!')
}
}
}
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
export const dryRunWrapper = async (
callback: () => Promise<
| Models['WebSocketResponse_type']
| [Models['WebSocketResponse_type']]
| undefined
| null
>
): Promise<[Models['WebSocketResponse_type']] | undefined> => {
// Gotcha: What about race conditions?
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'enable_dry_run' },
})
const result = await callback()
if (!result) {
return undefined
}
if (isArray(result)) {
return result
}
return [result]
} catch (e) {
console.error(e)
} finally {
await disableDryRunWithRetry(5)
}
}
function isSelections(selections: unknown): selections is Selections {
return (
(selections as Selections).graphSelections !== undefined &&
(selections as Selections).otherSelections !== undefined
)
}
export function parseEngineErrorMessage(
engineErrors?: [Models['WebSocketResponse_type']]
): string | undefined {
if (!engineErrors) {
return undefined
}
if (!engineErrors[0]) {
return undefined
}
const engineError = engineErrors[0]
if (engineError.success) {
return undefined
}
const errors = engineError.errors
if (!errors[0]) {
return undefined
}
return errors[0].message
}
export const revolveAxisValidator = async ({
data,
context,
}: {
data: { [key: string]: Selections }
context: CommandBarContext
}): Promise<boolean | string> => {
if (!isSelections(context.argumentsToSubmit.sketches)) {
return 'Unable to revolve, selections are missing'
}
// Gotcha: this validation only works for the first sketch
const artifact =
context.argumentsToSubmit.sketches.graphSelections[0].artifact
if (!artifact) {
return 'Unable to revolve, sketch not found'
}
if (!('pathId' in artifact)) {
return 'Unable to revolve, sketch has no path'
}
const sketchSelection = artifact.pathId
let edgeSelection = data.edge.graphSelections[0].artifact?.id
if (!sketchSelection) {
return 'Unable to revolve, sketch is missing'
}
if (!edgeSelection) {
return 'Unable to revolve, edge is missing'
}
const angleInDegrees: Models['Angle_type'] = {
unit: 'degrees',
value: 360,
}
const command = async () => {
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'revolve_about_edge',
angle: angleInDegrees,
edge_id: edgeSelection,
target: sketchSelection,
// Gotcha: Playwright will fail with larger tolerances, need to use a smaller one.
tolerance: 1e-7,
// WARNING: I'm not sure this is what it should be.
opposite: 'None',
},
})
}
const result = await dryRunWrapper(command)
if (result && result[0] && result[0].success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to revolve with the current selection. Reason: ${reason}`
}
export const loftValidator = async ({
data,
}: {
data: { [key: string]: Selections }
context: CommandBarContext
}): Promise<boolean | string> => {
if (!isSelections(data.sketches)) {
return 'Unable to loft, selections are missing'
}
const { sketches } = data
if (sketches.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) {
return 'Unable to loft, some selection are not solid2ds'
}
const sectionIds = sketches.graphSelections.flatMap((s) =>
s.artifact?.type === 'solid2d' ? s.artifact.pathId : []
)
if (sectionIds.length < 2) {
return 'Unable to loft, selection contains less than two solid2ds'
}
const command = async () => {
// TODO: check what to do with these
const DEFAULT_V_DEGREE = 2
const DEFAULT_TOLERANCE = 2
const DEFAULT_BEZ_APPROXIMATE_RATIONAL = false
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
section_ids: sectionIds,
type: 'loft',
bez_approximate_rational: DEFAULT_BEZ_APPROXIMATE_RATIONAL,
tolerance: DEFAULT_TOLERANCE,
v_degree: DEFAULT_V_DEGREE,
},
})
}
const result = await dryRunWrapper(command)
if (result && result[0] && result[0].success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to loft with the current selection. Reason: ${reason}`
}
export const shellValidator = async ({
data,
}: {
data: { selection: Selections }
}): Promise<boolean | string> => {
if (!isSelections(data.selection)) {
return 'Unable to shell, selections are missing'
}
// No validation on the faces, filtering is done upstream and we have the dry run validation just below
const face_ids = data.selection.graphSelections.flatMap((s) =>
s.artifact ? s.artifact.id : []
)
// We don't have the concept of solid3ds in TS yet.
// So we're listing out the sweeps as if they were solids and taking the first one, just like in Rust for Shell:
// https://github.com/KittyCAD/modeling-app/blob/e61fff115b9fa94aaace6307b1842cc15d41655e/src/wasm-lib/kcl/src/std/shell.rs#L237-L238
// TODO: This is one cheap way to make sketch-on-face supported now but will likely fail multiple solids
const object_id = kclManager.artifactGraph
.values()
.find((v) => v.type === 'sweep')?.pathId
if (!object_id) {
return "Unable to shell, couldn't find the solid"
}
const command = async () => {
// TODO: figure out something better than an arbitrarily small value
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false
const cmdArgs = {
face_ids,
object_id,
hollow: DEFAULT_HOLLOW,
shell_thickness: DEFAULT_THICKNESS,
}
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'solid3d_shell_face',
...cmdArgs,
},
})
}
const result = await dryRunWrapper(command)
if (result && result[0] && result[0].success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to shell with the current selection. Reason: ${reason}`
}
export const sweepValidator = async ({
context,
data,
}: {
context: CommandBarContext
data: { path: Selections }
}): Promise<boolean | string> => {
if (!isSelections(data.path)) {
console.log('Unable to sweep, selections are missing')
return 'Unable to sweep, selections are missing'
}
// Retrieve the parent path from the segment selection directly
const trajectoryArtifact = data.path.graphSelections[0].artifact
if (!trajectoryArtifact) {
return "Unable to sweep, couldn't find the trajectory artifact"
}
if (trajectoryArtifact.type !== 'segment') {
return "Unable to sweep, couldn't find the target from a non-segment selection"
}
const trajectory = trajectoryArtifact.pathId
// Get the former arg in the command bar flow, and retrieve the path from the solid2d directly
const targetArg = context.argumentsToSubmit['sketches'] as Selections
const targetArtifact = targetArg.graphSelections[0].artifact
if (!targetArtifact) {
return "Unable to sweep, couldn't find the profile artifact"
}
if (targetArtifact.type !== 'solid2d') {
return "Unable to sweep, couldn't find the target from a non-solid2d selection"
}
const target = targetArtifact.pathId
const command = async () => {
// TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false
const cmdArgs = {
target,
trajectory,
sectional: DEFAULT_SECTIONAL,
tolerance: DEFAULT_TOLERANCE,
}
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sweep',
...cmdArgs,
},
})
}
const result = await dryRunWrapper(command)
if (result && result[0] && result[0].success) {
return true
}
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to sweep with the current selection. Reason: ${reason}`
}