Add Rust side artifacts for startSketchOn face or plane (#4834)

* Add Rust side artifacts for startSketchOn face or plane

* move ast digging

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
This commit is contained in:
Jonathan Tran
2024-12-18 19:03:21 -05:00
committed by GitHub
parent 2c40e8a97c
commit 3865637c61
9 changed files with 219 additions and 133 deletions

View File

@ -357,7 +357,10 @@ export class KclManager {
}
this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph(this.ast)
await this.engineCommandManager.updateArtifactGraph(
this.ast,
execState.artifacts
)
this._executeCallback()
if (!isInterrupted)
sceneInfra.modelingSend({ type: 'code edit during sketch' })

View File

@ -212,7 +212,19 @@ Map {
"type": "wall",
},
"UUID-10" => {
"codeRef": undefined,
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
312,
344,
true,
],
},
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [

View File

@ -1,4 +1,10 @@
import { makeDefaultPlanes, assertParse, initPromise, Program } from 'lang/wasm'
import {
makeDefaultPlanes,
assertParse,
initPromise,
Program,
ExecState,
} from 'lang/wasm'
import { Models } from '@kittycad/lib'
import {
OrderedCommand,
@ -123,6 +129,7 @@ type CacheShape = {
[key in CodeKey]: {
orderedCommands: OrderedCommand[]
responseMap: ResponseMap
execStateArtifacts: ExecState['artifacts']
}
}
@ -154,6 +161,7 @@ beforeAll(async () => {
cacheToWriteToFileTemp[codeKey] = {
orderedCommands: engineCommandManager.orderedCommands,
responseMap: engineCommandManager.responseMap,
execStateArtifacts: kclManager.execState.artifacts,
}
}
const cache = JSON.stringify(cacheToWriteToFileTemp)
@ -181,9 +189,15 @@ describe('testing createArtifactGraph', () => {
orderedCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeOffsetPlanes')
ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
theMap = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it(`there should be one sketch`, () => {
@ -226,9 +240,15 @@ describe('testing createArtifactGraph', () => {
orderedCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCode1')
ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
theMap = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it('there should be two planes for the extrusion and the sketch on face', () => {
@ -321,9 +341,15 @@ describe('testing createArtifactGraph', () => {
orderedCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeNo3D')
ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
theMap = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
})
it('there should be two planes, one for each sketch path', () => {
@ -386,9 +412,15 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
orderedCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('sketchOnFaceOnFaceEtc')
ast = _ast
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
theMap = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
// Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one
@ -409,10 +441,12 @@ function getCommands(
// these either already exist from the last run, or were created in
const orderedCommands = parsed[codeKey].orderedCommands
const responseMap = parsed[codeKey].responseMap
const execStateArtifacts = parsed[codeKey].execStateArtifacts
return {
orderedCommands,
responseMap,
ast,
execStateArtifacts,
}
}
@ -638,8 +672,14 @@ async function GraphTheGraph(
describe('testing getArtifactsToUpdate', () => {
it('should return an array of artifacts to update', () => {
const { orderedCommands, responseMap, ast } = getCommands('exampleCode1')
const map = createArtifactGraph({ orderedCommands, responseMap, ast })
const { orderedCommands, responseMap, ast, execStateArtifacts } =
getCommands('exampleCode1')
const map = createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
})
const getArtifact = (id: string) => map.get(id)
const currentPlaneId = 'UUID-1'
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => {
@ -652,6 +692,7 @@ describe('testing getArtifactsToUpdate', () => {
getArtifact,
currentPlaneId,
ast,
execStateArtifacts,
})
return artifactsToUpdate.map(({ artifact }) => artifact)
}
@ -779,6 +820,10 @@ describe('testing getArtifactsToUpdate', () => {
},
{
type: 'wall',
codeRef: {
pathToNode: [['body', '']],
range: [312, 344, true],
},
id: expect.any(String),
segId: expect.any(String),
edgeCutEdgeIds: [],

View File

@ -1,16 +1,6 @@
import {
Expr,
PathToNode,
Program,
SourceRange,
VariableDeclaration,
} from 'lang/wasm'
import { ExecState, Expr, PathToNode, Program, SourceRange } from 'lang/wasm'
import { Models } from '@kittycad/lib'
import {
getNodeFromPath,
getNodePathFromSourceRange,
traverse,
} from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node'
@ -174,10 +164,12 @@ export function createArtifactGraph({
orderedCommands,
responseMap,
ast,
execStateArtifacts,
}: {
orderedCommands: Array<OrderedCommand>
responseMap: ResponseMap
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}) {
const myMap = new Map<ArtifactId, Artifact>()
@ -199,6 +191,7 @@ export function createArtifactGraph({
getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId,
ast,
execStateArtifacts,
})
artifactsToUpdate.forEach(({ id, artifact }) => {
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
@ -250,6 +243,7 @@ export function getArtifactsToUpdate({
responseMap,
currentPlaneId,
ast,
execStateArtifacts,
}: {
orderedCommand: OrderedCommand
responseMap: ResponseMap
@ -257,6 +251,7 @@ export function getArtifactsToUpdate({
getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
}): Array<{
id: ArtifactId
artifact: Artifact
@ -292,13 +287,6 @@ export function getArtifactsToUpdate({
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
const existingPlane = getArtifact(currentPlaneId)
if (existingPlane?.type === 'wall') {
let existingPlaneCodeRef = existingPlane.codeRef
if (!existingPlaneCodeRef) {
const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode)
if (!err(astWalkCodeRef)) {
existingPlaneCodeRef = astWalkCodeRef
}
}
return [
{
id: currentPlaneId,
@ -309,18 +297,11 @@ export function getArtifactsToUpdate({
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds,
codeRef: existingPlaneCodeRef,
codeRef: existingPlane.codeRef,
},
},
]
} else if (existingPlane?.type === 'cap') {
let existingPlaneCodeRef = existingPlane.codeRef
if (!existingPlaneCodeRef) {
const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode)
if (!err(astWalkCodeRef)) {
existingPlaneCodeRef = astWalkCodeRef
}
}
return [
{
id: currentPlaneId,
@ -331,7 +312,7 @@ export function getArtifactsToUpdate({
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds,
codeRef: existingPlaneCodeRef,
codeRef: existingPlane.codeRef,
},
},
]
@ -467,16 +448,33 @@ export function getArtifactsToUpdate({
const path = getArtifact(seg.pathId)
if (path?.type === 'path' && seg?.type === 'segment') {
lastPath = path
returnArr.push({
id: face_id,
artifact: {
const extraArtifact = Object.values(execStateArtifacts).find(
(a) => a?.type === 'StartSketchOnFace' && a.faceId === face_id
)
const sketchOnFaceSourceRange = extraArtifact?.sourceRange
const wallArtifact: Artifact = {
type: 'wall',
id: face_id,
segId: curve_id,
edgeCutEdgeIds: [],
sweepId: path.sweepId,
pathIds: [],
},
}
if (sketchOnFaceSourceRange) {
const range: SourceRange = [
sketchOnFaceSourceRange[0],
sketchOnFaceSourceRange[1],
true,
]
wallArtifact.codeRef = {
range,
pathToNode: getNodePathFromSourceRange(ast, range),
}
}
returnArr.push({
id: face_id,
artifact: wallArtifact,
})
returnArr.push({
id: curve_id,
@ -500,16 +498,33 @@ export function getArtifactsToUpdate({
if ((cap === 'top' || cap === 'bottom') && face_id) {
const path = lastPath
if (path?.type === 'path') {
returnArr.push({
id: face_id,
artifact: {
const extraArtifact = Object.values(execStateArtifacts).find(
(a) => a?.type === 'StartSketchOnFace' && a.faceId === face_id
)
const sketchOnFaceSourceRange = extraArtifact?.sourceRange
const capArtifact: Artifact = {
type: 'cap',
id: face_id,
subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [],
sweepId: path.sweepId,
pathIds: [],
},
}
if (sketchOnFaceSourceRange) {
const range: SourceRange = [
sketchOnFaceSourceRange[0],
sketchOnFaceSourceRange[1],
true,
]
capArtifact.codeRef = {
range,
pathToNode: getNodePathFromSourceRange(ast, range),
}
}
returnArr.push({
id: face_id,
artifact: capArtifact,
})
const sweep = getArtifact(path.sweepId)
if (sweep?.type !== 'sweep') return
@ -1131,84 +1146,6 @@ function isNodeSafe(node: Expr): boolean {
return false
}
/** {@deprecated} this information should come from the ArtifactGraph not digging around in the AST */
function getWallOrCapPlaneCodeRef(
ast: Node<Program>,
pathToNode: PathToNode
): CodeRef | Error {
const varDec = getNodeFromPath<VariableDeclaration>(
ast,
pathToNode,
'VariableDeclaration'
)
if (err(varDec)) return varDec
if (varDec.node.type !== 'VariableDeclaration')
return new Error('Expected VariableDeclaration')
const init = varDec.node.declaration.init
let varName = ''
if (
init.type === 'CallExpression' &&
init.callee.type === 'Identifier' &&
(init.callee.name === 'circle' || init.callee.name === 'startProfileAt')
) {
const secondArg = init.arguments[1]
if (secondArg.type === 'Identifier') {
varName = secondArg.name
}
} else if (init.type === 'PipeExpression') {
const firstExpr = init.body[0]
if (
firstExpr.type === 'CallExpression' &&
firstExpr.callee.type === 'Identifier' &&
firstExpr.callee.name === 'startProfileAt'
) {
const secondArg = firstExpr.arguments[1]
if (secondArg.type === 'Identifier') {
varName = secondArg.name
}
}
}
if (varName === '') return new Error('Could not find variable name')
let currentVariableName = ''
const planeCodeRef: Array<{
path: PathToNode
sketchName: string
range: SourceRange
}> = []
traverse(ast, {
leave: (node) => {
if (node.type === 'VariableDeclaration') {
currentVariableName = ''
}
},
enter: (node, path) => {
if (node.type === 'VariableDeclaration') {
currentVariableName = node.declaration.id.name
}
if (
// match `${varName} = startSketchOn(...)`
node.type === 'CallExpression' &&
node.callee.name === 'startSketchOn' &&
node.arguments[0].type === 'Identifier' &&
currentVariableName === varName
) {
planeCodeRef.push({
path,
sketchName: currentVariableName,
range: [node.start, node.end, true],
})
}
},
})
if (!planeCodeRef.length)
return new Error('No paths found depending on extrude')
return {
pathToNode: planeCodeRef[0].path,
range: planeCodeRef[0].range,
}
}
/**
* Get an artifact from a code source range
*/

View File

@ -1,6 +1,7 @@
import {
defaultRustSourceRange,
defaultSourceRange,
ExecState,
Program,
RustSourceRange,
SourceRange,
@ -2116,11 +2117,15 @@ export class EngineCommandManager extends EventTarget {
Object.values(this.pendingCommands).map((a) => a.promise)
)
}
updateArtifactGraph(ast: Node<Program>) {
updateArtifactGraph(
ast: Node<Program>,
execStateArtifacts: ExecState['artifacts']
) {
this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands,
responseMap: this.responseMap,
ast,
execStateArtifacts,
})
// TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) {

View File

@ -44,7 +44,11 @@ import { CompilationError } from 'wasm-lib/kcl/bindings/CompilationError'
import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
import { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
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'
@ -245,6 +249,7 @@ export const isPathToNodeNumber = (
export interface ExecState {
memory: ProgramMemory
artifacts: { [key in ArtifactId]?: Artifact }
}
/**
@ -254,12 +259,14 @@ export interface ExecState {
export function emptyExecState(): ExecState {
return {
memory: ProgramMemory.empty(),
artifacts: {},
}
}
function execStateFromRaw(raw: RawExecState): ExecState {
return {
memory: ProgramMemory.fromRaw(raw.modLocal.memory),
artifacts: raw.global.artifacts,
}
}

View File

@ -0,0 +1,47 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::SourceRange;
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct ArtifactId(Uuid);
impl ArtifactId {
pub fn new(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl From<Uuid> for ArtifactId {
fn from(uuid: Uuid) -> Self {
Self::new(uuid)
}
}
impl From<&Uuid> for ArtifactId {
fn from(uuid: &Uuid) -> Self {
Self::new(*uuid)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
pub id: ArtifactId,
#[serde(flatten)]
pub inner: ArtifactInner,
pub source_range: SourceRange,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum ArtifactInner {
#[serde(rename_all = "camelCase")]
StartSketchOnFace { face_id: Uuid },
#[serde(rename_all = "camelCase")]
StartSketchOnPlane { plane_id: Uuid },
}

View File

@ -25,6 +25,7 @@ pub use kcl_value::{KclObjectFields, KclValue};
use uuid::Uuid;
mod annotations;
mod artifact;
pub(crate) mod cache;
mod cad_op;
mod exec_ast;
@ -47,6 +48,7 @@ use crate::{
};
// Re-exports.
pub use artifact::{Artifact, ArtifactId, ArtifactInner};
pub use cad_op::Operation;
/// State for executing a program.
@ -68,6 +70,8 @@ pub struct GlobalState {
pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>,
/// Map from module ID to module info.
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
/// Output map of UUIDs to artifacts.
pub artifacts: IndexMap<ArtifactId, Artifact>,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -134,6 +138,11 @@ impl ExecState {
self.global.id_generator.next_uuid()
}
pub fn add_artifact(&mut self, artifact: Artifact) {
let id = artifact.id;
self.global.artifacts.insert(id, artifact);
}
async fn add_module(
&mut self,
path: std::path::PathBuf,
@ -171,6 +180,7 @@ impl GlobalState {
id_generator: Default::default(),
path_to_source_id: Default::default(),
module_infos: Default::default(),
artifacts: Default::default(),
};
// TODO(#4434): Use the top-level file's path.

View File

@ -11,6 +11,7 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::execution::{Artifact, ArtifactId, ArtifactInner};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
@ -1075,7 +1076,17 @@ async fn inner_start_sketch_on(
let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
Ok(SketchSurface::Plane(plane))
}
SketchData::Plane(plane) => Ok(SketchSurface::Plane(plane)),
SketchData::Plane(plane) => {
// Create artifact used only by the UI, not the engine.
let id = exec_state.next_uuid();
exec_state.add_artifact(Artifact {
id: ArtifactId::from(id),
inner: ArtifactInner::StartSketchOnPlane { plane_id: plane.id },
source_range: args.source_range,
});
Ok(SketchSurface::Plane(plane))
}
SketchData::Solid(solid) => {
let Some(tag) = tag else {
return Err(KclError::Type(KclErrorDetails {
@ -1084,6 +1095,15 @@ async fn inner_start_sketch_on(
}));
};
let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
// Create artifact used only by the UI, not the engine.
let id = exec_state.next_uuid();
exec_state.add_artifact(Artifact {
id: ArtifactId::from(id),
inner: ArtifactInner::StartSketchOnFace { face_id: face.id },
source_range: args.source_range,
});
Ok(SketchSurface::Face(face))
}
}