Adding point and click revolve workflow for sketch and axis selection (#4687)
* selection stuff
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest)
* trigger CI
* fix bugs
* some edge cut stuff
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)
* trigger CI
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)
* fix sketch mode issues
* fix more tests, selection in sketch related
* more test fixing
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)
* Trigger ci
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)
* Trigger ci
* more sketch mode selection fixes
* fix unit tests
* rename function
* remove .only
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* lint
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* fix bad pathToNode issue
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores)
* fix sketch on face
* migrate a more selections types
* migrate a more selections types
* fix code selection of fillets
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* migrate a more selections types
* fix bad path to node, looks like a race
* migrate a more selections types
* migrate a more selections types
* fix cmd bar selections
* fix cmd bar selections
* fix display issues
* feat: implementing axis selection for point and click revolve
* feat: enforcing selection of 2 options for axis rotation
* feat: added negative rotations for the revolve
* fix: fmt, tsc fixes
* migrate a more selections types
* Revert "migrate a more selections types"
This reverts commit 0d0e453bbb
.
* migrate a more selections types
* clean up1
* clean up 2
* chore: improving the copy after discussing with Frank
* fix: merge main fixes
* chore: was able to add a seg to a line. Does not check if one exists already
* saving off some code
* chore: moving revolveSketch into own file for readability, improving variable names instead of node1
* chore: renaming more variables for readability
* chore: more renaming
* fix: allows creating a custom rotation on axis
* fix: added opposite edge logic and adj, need to error handle still
* fix: use other import
* feat: point and click on edges, crude implementation
* feat: implemented toast message and returned error message from validation
* fix: auto linter
* fix: addressing tsc errors
* fix: fighting typescript
* fix: cleaning up PR
* fix: trying to resolve more typescript issues
* fix: save off tsc fixes
* fix: adding comments
* fix: resolving tsc errors
* fix: tsc errors
* fix: auto linter fixes and tsc fixes
* fix:??
* fix: revolve ast works with declaration
* fix: retry logic to make sure the disable dry run actually runs
* fix: codespell typo
---------
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -54,6 +54,7 @@ import {
|
|||||||
Selections,
|
Selections,
|
||||||
updateSelections,
|
updateSelections,
|
||||||
canLoftSelection,
|
canLoftSelection,
|
||||||
|
canRevolveSelection,
|
||||||
canShellSelection,
|
canShellSelection,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
@ -575,6 +576,26 @@ export const ModelingMachineProvider = ({
|
|||||||
if (err(canSweep)) return false
|
if (err(canSweep)) return false
|
||||||
return canSweep
|
return canSweep
|
||||||
},
|
},
|
||||||
|
'has valid revolve selection': ({ context: { selectionRanges } }) => {
|
||||||
|
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||||
|
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||||
|
const hasNoSelection =
|
||||||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
isRangeBetweenCharacters(selectionRanges) ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
|
||||||
|
if (hasNoSelection) {
|
||||||
|
// they have no selection, we should enable the button
|
||||||
|
// so they can select the face through the cmdbar
|
||||||
|
// BUT only if there's extrudable geometry
|
||||||
|
return doesSceneHaveSweepableSketch(kclManager.ast)
|
||||||
|
}
|
||||||
|
if (!isSketchPipe(selectionRanges)) return false
|
||||||
|
|
||||||
|
const canSweep = canRevolveSelection(selectionRanges)
|
||||||
|
if (err(canSweep)) return false
|
||||||
|
return canSweep
|
||||||
|
},
|
||||||
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
||||||
const hasNoSelection =
|
const hasNoSelection =
|
||||||
selectionRanges.graphSelections.length === 0 ||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
@ -335,7 +335,7 @@ export function mutateAstWithTagForSketchSegment(
|
|||||||
return { modifiedAst: astClone, tag }
|
return { modifiedAst: astClone, tag }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeTagCall(
|
export function getEdgeTagCall(
|
||||||
tag: string,
|
tag: string,
|
||||||
artifact: Artifact
|
artifact: Artifact
|
||||||
): Node<Identifier | CallExpression> {
|
): Node<Identifier | CallExpression> {
|
||||||
|
154
src/lang/modifyAst/addRevolve.ts
Normal file
154
src/lang/modifyAst/addRevolve.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { err } from 'lib/trap'
|
||||||
|
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||||
|
import {
|
||||||
|
Program,
|
||||||
|
PathToNode,
|
||||||
|
Expr,
|
||||||
|
CallExpression,
|
||||||
|
PipeExpression,
|
||||||
|
VariableDeclarator,
|
||||||
|
} from 'lang/wasm'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
|
import {
|
||||||
|
createLiteral,
|
||||||
|
createCallExpressionStdLib,
|
||||||
|
createObjectExpression,
|
||||||
|
createIdentifier,
|
||||||
|
createPipeExpression,
|
||||||
|
findUniqueName,
|
||||||
|
createVariableDeclaration,
|
||||||
|
} from 'lang/modifyAst'
|
||||||
|
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||||
|
import {
|
||||||
|
mutateAstWithTagForSketchSegment,
|
||||||
|
getEdgeTagCall,
|
||||||
|
} from 'lang/modifyAst/addEdgeTreatment'
|
||||||
|
export function revolveSketch(
|
||||||
|
ast: Node<Program>,
|
||||||
|
pathToSketchNode: PathToNode,
|
||||||
|
shouldPipe = false,
|
||||||
|
angle: Expr = createLiteral(4),
|
||||||
|
axis: Selections
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
modifiedAst: Node<Program>
|
||||||
|
pathToSketchNode: PathToNode
|
||||||
|
pathToRevolveArg: PathToNode
|
||||||
|
}
|
||||||
|
| Error {
|
||||||
|
const clonedAst = structuredClone(ast)
|
||||||
|
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
|
||||||
|
if (err(sketchNode)) return sketchNode
|
||||||
|
|
||||||
|
// testing code
|
||||||
|
const pathToAxisSelection = getNodePathFromSourceRange(
|
||||||
|
clonedAst,
|
||||||
|
axis.graphSelections[0]?.codeRef.range
|
||||||
|
)
|
||||||
|
|
||||||
|
const lineNode = getNodeFromPath<CallExpression>(
|
||||||
|
clonedAst,
|
||||||
|
pathToAxisSelection,
|
||||||
|
'CallExpression'
|
||||||
|
)
|
||||||
|
if (err(lineNode)) return lineNode
|
||||||
|
|
||||||
|
// TODO Kevin: What if |> close(%)?
|
||||||
|
// TODO Kevin: What if opposite edge
|
||||||
|
// TODO Kevin: What if the edge isn't planar to the sketch?
|
||||||
|
// TODO Kevin: add a tag.
|
||||||
|
const tagResult = mutateAstWithTagForSketchSegment(
|
||||||
|
clonedAst,
|
||||||
|
pathToAxisSelection
|
||||||
|
)
|
||||||
|
|
||||||
|
// Have the tag whether it is already created or a new one is generated
|
||||||
|
if (err(tagResult)) return tagResult
|
||||||
|
const { tag } = tagResult
|
||||||
|
|
||||||
|
/* Original Code */
|
||||||
|
const { node: sketchExpression } = sketchNode
|
||||||
|
|
||||||
|
// determine if sketchExpression is in a pipeExpression or not
|
||||||
|
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
|
||||||
|
clonedAst,
|
||||||
|
pathToSketchNode,
|
||||||
|
'PipeExpression'
|
||||||
|
)
|
||||||
|
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
|
||||||
|
const { node: sketchPipeExpression } = sketchPipeExpressionNode
|
||||||
|
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
|
||||||
|
|
||||||
|
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
clonedAst,
|
||||||
|
pathToSketchNode,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
|
||||||
|
const {
|
||||||
|
node: sketchVariableDeclarator,
|
||||||
|
shallowPath: sketchPathToDecleration,
|
||||||
|
} = sketchVariableDeclaratorNode
|
||||||
|
|
||||||
|
const axisSelection = axis?.graphSelections[0]?.artifact
|
||||||
|
|
||||||
|
if (!axisSelection) return new Error('Axis selection is missing.')
|
||||||
|
|
||||||
|
const revolveCall = createCallExpressionStdLib('revolve', [
|
||||||
|
createObjectExpression({
|
||||||
|
angle: angle,
|
||||||
|
axis: getEdgeTagCall(tag, axisSelection),
|
||||||
|
}),
|
||||||
|
createIdentifier(sketchVariableDeclarator.id.name),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (shouldPipe) {
|
||||||
|
const pipeChain = createPipeExpression(
|
||||||
|
isInPipeExpression
|
||||||
|
? [...sketchPipeExpression.body, revolveCall]
|
||||||
|
: [sketchExpression as any, revolveCall]
|
||||||
|
)
|
||||||
|
|
||||||
|
sketchVariableDeclarator.init = pipeChain
|
||||||
|
const pathToRevolveArg: PathToNode = [
|
||||||
|
...sketchPathToDecleration,
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['body', ''],
|
||||||
|
[pipeChain.body.length - 1, 'index'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst: clonedAst,
|
||||||
|
pathToSketchNode,
|
||||||
|
pathToRevolveArg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're not creating a pipe expression,
|
||||||
|
// but rather a separate constant for the extrusion
|
||||||
|
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
|
||||||
|
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
|
||||||
|
const sketchIndexInPathToNode =
|
||||||
|
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
|
||||||
|
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
|
||||||
|
if (typeof sketchIndexInBody !== 'number')
|
||||||
|
return new Error('expected sketchIndexInBody to be a number')
|
||||||
|
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
||||||
|
|
||||||
|
const pathToRevolveArg: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[sketchIndexInBody + 1, 'index'],
|
||||||
|
['declaration', 'VariableDeclaration'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
modifiedAst: clonedAst,
|
||||||
|
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
|
||||||
|
pathToRevolveArg,
|
||||||
|
}
|
||||||
|
}
|
@ -871,3 +871,15 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
|
|||||||
pathToNode: getNodePathFromSourceRange(ast, range),
|
pathToNode: getNodePathFromSourceRange(ast, range),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSolid2D(artifact: Artifact): artifact is solid2D {
|
||||||
|
return (artifact as solid2D).pathId !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSegment(artifact: Artifact): artifact is SegmentArtifact {
|
||||||
|
return (artifact as SegmentArtifact).pathId !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSweep(artifact: Artifact): artifact is SweepArtifact {
|
||||||
|
return (artifact as SweepArtifact).pathId !== undefined
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { Selections } from 'lib/selections'
|
|||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
||||||
|
import { revolveAxisValidator } from './validators'
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
type OutputTypeKey = OutputFormat['type']
|
type OutputTypeKey = OutputFormat['type']
|
||||||
@ -46,6 +47,7 @@ export type ModelingCommandSchema = {
|
|||||||
Revolve: {
|
Revolve: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
angle: KclCommandValue
|
angle: KclCommandValue
|
||||||
|
axis: Selections
|
||||||
}
|
}
|
||||||
Fillet: {
|
Fillet: {
|
||||||
// todo
|
// todo
|
||||||
@ -330,6 +332,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
required: true,
|
required: true,
|
||||||
skip: true,
|
skip: true,
|
||||||
},
|
},
|
||||||
|
axis: {
|
||||||
|
required: true,
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
|
||||||
|
multiple: false,
|
||||||
|
validation: revolveAxisValidator,
|
||||||
|
},
|
||||||
angle: {
|
angle: {
|
||||||
inputType: 'kcl',
|
inputType: 'kcl',
|
||||||
defaultValue: KCL_DEFAULT_DEGREE,
|
defaultValue: KCL_DEFAULT_DEGREE,
|
||||||
|
107
src/lib/commandBarConfigs/validators.ts
Normal file
107
src/lib/commandBarConfigs/validators.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
|
import { uuidv4 } from 'lib/utils'
|
||||||
|
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
import { isSolid2D, isSegment, isSweep } from 'lang/std/artifactGraph'
|
||||||
|
|
||||||
|
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<any>) => {
|
||||||
|
// 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()
|
||||||
|
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 const revolveAxisValidator = async ({
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
data: { [key: string]: Selections }
|
||||||
|
context: CommandBarContext
|
||||||
|
}): Promise<boolean | string> => {
|
||||||
|
if (!isSelections(context.argumentsToSubmit.selection)) {
|
||||||
|
return 'Unable to revolve, selections are missing'
|
||||||
|
}
|
||||||
|
const artifact =
|
||||||
|
context.argumentsToSubmit.selection.graphSelections[0].artifact
|
||||||
|
|
||||||
|
if (!artifact) {
|
||||||
|
return 'Unable to revolve, sketch not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(isSolid2D(artifact) || isSegment(artifact) || isSweep(artifact))) {
|
||||||
|
return 'Unable to revolve, sketch has no path'
|
||||||
|
}
|
||||||
|
|
||||||
|
const sketchSelection = artifact.pathId
|
||||||
|
let edgeSelection = data.axis.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 revolveAboutEdgeCommand = async () => {
|
||||||
|
return await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'revolve_about_edge',
|
||||||
|
angle: angleInDegrees,
|
||||||
|
edge_id: edgeSelection,
|
||||||
|
target: sketchSelection,
|
||||||
|
tolerance: 0.0001,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
|
||||||
|
if (attemptRevolve?.success) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// return error message for the toast
|
||||||
|
return 'Unable to revolve with selected axis'
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ import { ReactNode } from 'react'
|
|||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||||
import { Artifact } from 'lang/std/artifactGraph'
|
import { Artifact } from 'lang/std/artifactGraph'
|
||||||
|
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||||
type Icon = CustomIconName
|
type Icon = CustomIconName
|
||||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
const INPUT_TYPES = [
|
const INPUT_TYPES = [
|
||||||
@ -147,6 +147,13 @@ export type CommandArgumentConfig<
|
|||||||
inputType: 'selection'
|
inputType: 'selection'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
|
validation?: ({
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
data: any
|
||||||
|
context: CommandBarContext
|
||||||
|
}) => Promise<boolean | string>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: 'kcl'
|
inputType: 'kcl'
|
||||||
@ -236,6 +243,13 @@ export type CommandArgument<
|
|||||||
inputType: 'selection'
|
inputType: 'selection'
|
||||||
selectionTypes: Artifact['type'][]
|
selectionTypes: Artifact['type'][]
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
|
validation?: ({
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
data: any
|
||||||
|
context: CommandBarContext
|
||||||
|
}) => Promise<boolean | string>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: 'kcl'
|
inputType: 'kcl'
|
||||||
|
@ -111,3 +111,10 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
|
|||||||
|
|
||||||
/** Toast id for the app auto-updater toast */
|
/** Toast id for the app auto-updater toast */
|
||||||
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
|
||||||
|
|
||||||
|
/** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */
|
||||||
|
export const KCL_AXIS_X = 'X'
|
||||||
|
export const KCL_AXIS_Y = 'Y'
|
||||||
|
export const KCL_AXIS_NEG_X = '-X'
|
||||||
|
export const KCL_AXIS_NEG_Y = '-Y'
|
||||||
|
export const KCL_DEFAULT_AXIS = 'X'
|
||||||
|
@ -155,6 +155,8 @@ export function buildCommandArgument<
|
|||||||
context: ContextFrom<T>,
|
context: ContextFrom<T>,
|
||||||
machineActor: Actor<T>
|
machineActor: Actor<T>
|
||||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
|
// GOTCHA: modelingCommandConfig is not a 1:1 mapping to this baseCommandArgument
|
||||||
|
// You need to manually add key/value pairs here.
|
||||||
const baseCommandArgument = {
|
const baseCommandArgument = {
|
||||||
description: arg.description,
|
description: arg.description,
|
||||||
required: arg.required,
|
required: arg.required,
|
||||||
@ -181,6 +183,7 @@ export function buildCommandArgument<
|
|||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
multiple: arg.multiple,
|
multiple: arg.multiple,
|
||||||
selectionTypes: arg.selectionTypes,
|
selectionTypes: arg.selectionTypes,
|
||||||
|
validation: arg.validation,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
} else if (arg.inputType === 'kcl') {
|
} else if (arg.inputType === 'kcl') {
|
||||||
return {
|
return {
|
||||||
|
@ -569,6 +569,17 @@ export function canSweepSelection(selection: Selections) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canRevolveSelection(selection: Selections) {
|
||||||
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
!!isSketchPipe(selection) &&
|
||||||
|
(commonNodes.every((n) => nodeHasClose(n)) ||
|
||||||
|
commonNodes.every((n) => nodeHasCircle(n)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function canLoftSelection(selection: Selections) {
|
export function canLoftSelection(selection: Selections) {
|
||||||
const commonNodes = selection.graphSelections.map((_, i) =>
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
buildCommonNodeFromSelection(selection, i)
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
import { Selections__old } from 'lib/selections'
|
import { Selections__old } from 'lib/selections'
|
||||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||||
import { MachineManager } from 'components/MachineManagerProvider'
|
import { MachineManager } from 'components/MachineManagerProvider'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
export type CommandBarContext = {
|
export type CommandBarContext = {
|
||||||
commands: Command[]
|
commands: Command[]
|
||||||
@ -247,14 +248,69 @@ export const commandBarMachine = setup({
|
|||||||
'All arguments are skippable': () => false,
|
'All arguments are skippable': () => false,
|
||||||
},
|
},
|
||||||
actors: {
|
actors: {
|
||||||
'Validate argument': fromPromise(({ input }) => {
|
'Validate argument': fromPromise(
|
||||||
return new Promise((resolve, reject) => {
|
({
|
||||||
// TODO: figure out if we should validate argument data here or in the form itself,
|
input,
|
||||||
// and if we should support people configuring a argument's validation function
|
}: {
|
||||||
|
input: {
|
||||||
|
context: CommandBarContext | undefined
|
||||||
|
event: CommandBarMachineEvent | undefined
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!input || input?.event?.type !== 'Submit argument') {
|
||||||
|
toast.error(`Unable to validate, wrong event type.`)
|
||||||
|
return reject(`Unable to validate, wrong event type`)
|
||||||
|
}
|
||||||
|
|
||||||
resolve(input)
|
const context = input?.context
|
||||||
})
|
|
||||||
}),
|
if (!context) {
|
||||||
|
toast.error(`Unable to validate, wrong argument.`)
|
||||||
|
return reject(`Unable to validate, wrong argument`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = input.event.data
|
||||||
|
const argName = context.currentArgument?.name
|
||||||
|
const args = context?.selectedCommand?.args
|
||||||
|
const argConfig = args && argName ? args[argName] : undefined
|
||||||
|
// Only do a validation check if the argument, selectedCommand, and the validation function are defined
|
||||||
|
if (
|
||||||
|
context.currentArgument &&
|
||||||
|
context.selectedCommand &&
|
||||||
|
argConfig?.inputType === 'selection' &&
|
||||||
|
argConfig?.validation
|
||||||
|
) {
|
||||||
|
argConfig
|
||||||
|
.validation({ context, data })
|
||||||
|
.then((result) => {
|
||||||
|
if (typeof result === 'boolean' && result === true) {
|
||||||
|
return resolve(data)
|
||||||
|
} else {
|
||||||
|
// validation failed
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
// The result of the validation is the error message
|
||||||
|
toast.error(result)
|
||||||
|
return reject(
|
||||||
|
`unable to validate ${argName}, Message: ${result}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Default message if there is not a custom one sent
|
||||||
|
toast.error(`Unable to validate ${argName}`)
|
||||||
|
return reject(`unable to validate ${argName}}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return reject(`unable to validate ${argName}}`)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Missing several requirements for validate argument, just bypass
|
||||||
|
return resolve(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
'Validate all arguments': fromPromise(
|
'Validate all arguments': fromPromise(
|
||||||
({ input }: { input: CommandBarContext }) => {
|
({ input }: { input: CommandBarContext }) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -449,9 +505,10 @@ export const commandBarMachine = setup({
|
|||||||
invoke: {
|
invoke: {
|
||||||
src: 'Validate argument',
|
src: 'Validate argument',
|
||||||
id: 'validateSingleArgument',
|
id: 'validateSingleArgument',
|
||||||
input: ({ event }) => {
|
input: ({ event, context }) => {
|
||||||
if (event.type !== 'Submit argument') return {}
|
if (event.type !== 'Submit argument')
|
||||||
return event.data
|
return { event: undefined, context: undefined }
|
||||||
|
return { event, context }
|
||||||
},
|
},
|
||||||
onDone: {
|
onDone: {
|
||||||
target: '#Command Bar.Checking Arguments',
|
target: '#Command Bar.Checking Arguments',
|
||||||
|
@ -42,12 +42,12 @@ import {
|
|||||||
applyConstraintEqualLength,
|
applyConstraintEqualLength,
|
||||||
setEqualLengthInfo,
|
setEqualLengthInfo,
|
||||||
} from 'components/Toolbar/EqualLength'
|
} from 'components/Toolbar/EqualLength'
|
||||||
|
import { revolveSketch } from 'lang/modifyAst/addRevolve'
|
||||||
import {
|
import {
|
||||||
addOffsetPlane,
|
addOffsetPlane,
|
||||||
deleteFromSelection,
|
deleteFromSelection,
|
||||||
extrudeSketch,
|
extrudeSketch,
|
||||||
loftSketches,
|
loftSketches,
|
||||||
revolveSketch,
|
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import {
|
import {
|
||||||
applyEdgeTreatmentToSelection,
|
applyEdgeTreatmentToSelection,
|
||||||
@ -394,6 +394,7 @@ export const modelingMachine = setup({
|
|||||||
guards: {
|
guards: {
|
||||||
'Selection is on face': () => false,
|
'Selection is on face': () => false,
|
||||||
'has valid sweep selection': () => false,
|
'has valid sweep selection': () => false,
|
||||||
|
'has valid revolve selection': () => false,
|
||||||
'has valid loft selection': () => false,
|
'has valid loft selection': () => false,
|
||||||
'has valid shell selection': () => false,
|
'has valid shell selection': () => false,
|
||||||
'has valid edge treatment selection': () => false,
|
'has valid edge treatment selection': () => false,
|
||||||
@ -682,7 +683,7 @@ export const modelingMachine = setup({
|
|||||||
if (event.type !== 'Revolve') return
|
if (event.type !== 'Revolve') return
|
||||||
;(async () => {
|
;(async () => {
|
||||||
if (!event.data) return
|
if (!event.data) return
|
||||||
const { selection, angle } = event.data
|
const { selection, angle, axis } = event.data
|
||||||
let ast = kclManager.ast
|
let ast = kclManager.ast
|
||||||
if (
|
if (
|
||||||
'variableName' in angle &&
|
'variableName' in angle &&
|
||||||
@ -693,15 +694,21 @@ export const modelingMachine = setup({
|
|||||||
newBody.splice(angle.insertIndex, 0, angle.variableDeclarationAst)
|
newBody.splice(angle.insertIndex, 0, angle.variableDeclarationAst)
|
||||||
ast.body = newBody
|
ast.body = newBody
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is the selection of the sketch that will be revolved
|
||||||
const pathToNode = getNodePathFromSourceRange(
|
const pathToNode = getNodePathFromSourceRange(
|
||||||
ast,
|
ast,
|
||||||
selection.graphSelections[0]?.codeRef.range
|
selection.graphSelections[0]?.codeRef.range
|
||||||
)
|
)
|
||||||
|
|
||||||
const revolveSketchRes = revolveSketch(
|
const revolveSketchRes = revolveSketch(
|
||||||
ast,
|
ast,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
false,
|
false,
|
||||||
'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst
|
'variableName' in angle
|
||||||
|
? angle.variableIdentifierAst
|
||||||
|
: angle.valueAst,
|
||||||
|
axis
|
||||||
)
|
)
|
||||||
if (trap(revolveSketchRes)) return
|
if (trap(revolveSketchRes)) return
|
||||||
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
|
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
|
||||||
@ -1687,7 +1694,7 @@ export const modelingMachine = setup({
|
|||||||
|
|
||||||
Revolve: {
|
Revolve: {
|
||||||
target: 'idle',
|
target: 'idle',
|
||||||
guard: 'has valid sweep selection',
|
guard: 'has valid revolve selection',
|
||||||
actions: ['AST revolve'],
|
actions: ['AST revolve'],
|
||||||
reenter: false,
|
reenter: false,
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user