Skip to content

Animate using a KCL function #7600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions public/kcl-samples/car-wheel-assembly/main.kcl
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import "car-tire.kcl" as carTire
import * from "parameters.kcl"

// Place the car rotor
carRotor
rotor = carRotor
|> translate(x = 0, y = 0.5, z = 0)

// Place the car wheel
carWheel

// Place the lug nuts
lugNut
lgnut = lugNut
|> patternCircular3d(
arcDegrees = 360,
axis = [0, 1, 0],
Expand All @@ -32,8 +32,19 @@ lugNut
)

// Place the brake caliper
brakeCaliper
cal = brakeCaliper
|> translate(x = 0, y = 0.5, z = 0)

// Place the car tire
carTire


fn animate(step: number(_)) {
angle = 0.6deg
rotate(rotor, pitch = angle)
rotate(lgnut, pitch = angle)
rotate(cal, pitch = angle)
rotate(carWheel, pitch = angle)
rotate(carTire, pitch = angle)
return 0
}
9 changes: 7 additions & 2 deletions public/kcl-samples/clock/main.kcl
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ profile007 = startProfile(
|> line(%, endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%)
profile008 = circle(sketch005, center = [0, 0], diameter = nubDiameter)
subtract2d(profile007, tool = profile008)
hourHand = subtract2d(profile007, tool = profile008)
|> extrude(%, length = 5)
|> appearance(%, color = "#404040")

Expand Down Expand Up @@ -413,7 +413,7 @@ profile009 = startProfile(
|> line(%, endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%)
profile010 = circle(sketch006, center = [0, 0], diameter = 30)
subtract2d(profile009, tool = profile010)
minuteHand = subtract2d(profile009, tool = profile010)
|> extrude(%, length = 5)
|> appearance(%, color = "#404040")

Expand All @@ -439,3 +439,8 @@ profile004 = startProfile(sketch003, at = [-slotWidth / 2, 200])
|> extrude(%, length = -20)

// todo: create cavity for the screw to slide into (need csg update)

fn animate(step: number(_)) {
rotate(hourHand, yaw = -0.1deg)
return rotate(minuteHand, yaw = -0.6deg)
}
37 changes: 37 additions & 0 deletions rust/kcl-lib/src/execution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,43 @@ impl ExecutorContext {
Ok(outcome)
}

pub async fn run_additional(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
assert!(!self.is_mock());

let (program, exec_state, result) = match cache::read_old_ast().await {
Some(cached_state) => {
let mut exec_state = cached_state.reconstitute_exec_state();
exec_state.mut_stack().restore_env(cached_state.main.result_env);

let result = self.run_concurrent(&program, &mut exec_state, None, true).await;

(program, exec_state, result)
}
None => {
let mut exec_state = ExecState::new(self);

let result = self.run_concurrent(&program, &mut exec_state, None, false).await;

(program, exec_state, result)
}
};

// Throw the error.
let result = result?;

// Save this as the last successful execution to the cache.
cache::write_old_ast(GlobalState::new(
exec_state.clone(),
self.settings.clone(),
program.ast,
result.0,
))
.await;

let outcome = exec_state.into_exec_outcome(result.0, self).await;
Ok(outcome)
}

/// Perform the execution of a program.
///
/// To access non-fatal errors and warnings, extract them from the `ExecState`.
Expand Down
42 changes: 42 additions & 0 deletions rust/kcl-wasm-lib/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,48 @@
ctx.run_with_caching(program).await
}

/// Execute an additional program using the cache.
#[wasm_bindgen(js_name = executeAdditional)]
pub async fn execute_additional(
&self,
program_ast_json: &str,
path: Option<String>,
settings: &str,
) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();

self.execute_additional_typed(program_ast_json, path, settings)
.await
.and_then(|outcome| {
JsValue::from_serde(&outcome).map_err(|e| {
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
KclErrorWithOutputs::no_outputs(KclError::internal(format!(
"Could not serialize successful KCL result. {TRUE_BUG} Details: {e}"
)))
})
})
.map_err(|e: KclErrorWithOutputs| JsValue::from_serde(&e).unwrap())
}

async fn execute_additional_typed(
&self,
program_ast_json: &str,
path: Option<String>,
settings: &str,
) -> Result<ExecOutcome, KclErrorWithOutputs> {
let program: Program = serde_json::from_str(program_ast_json).map_err(|e| {
let err = KclError::internal(format!("Could not deserialize KCL AST. {TRUE_BUG} Details: {e}"));
KclErrorWithOutputs::no_outputs(err)
})?;
let ctx = self.create_executor_ctx(settings, path, false).map_err(|e| {
KclErrorWithOutputs::no_outputs(KclError::internal(format!(
"Could not create KCL executor context. {TRUE_BUG} Details: {e}"
)))
})?;
ctx.run_additional(program).await
}

/// Reset the scene and bust the cache.
/// ONLY use this if you absolutely need to reset the scene and bust the cache.
#[wasm_bindgen(js_name = bustCacheAndResetScene)]
Expand Down Expand Up @@ -154,7 +196,7 @@
)))
})
})
.map_err(|e: KclErrorWithOutputs| JsValue::from_serde(&e).unwrap())

Check warning on line 199 in rust/kcl-wasm-lib/src/context.rs

View workflow job for this annotation

GitHub Actions / semgrep-oss/scan

panic-in-function-returning-result

expect or unwrap called in function returning a Result
}

async fn execute_mock_typed(
Expand Down
38 changes: 38 additions & 0 deletions src/components/ModelingSidebar/ModelingPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import Tooltip from '@src/components/Tooltip'

import styles from './ModelingPane.module.css'
import { reportRejection } from '@src/lib/trap'
import { kclManager } from '@src/lib/singletons'

export interface ModelingPaneProps {
id: string
Expand All @@ -19,6 +21,28 @@
onClose: () => void
}

const ANIMATE_INTERVAL = 16 // milliseconds
let animateTimeout: NodeJS.Timeout | null = null

function doAnimate() {
;(async () => {
await kclManager.executeAnimate()
if (animateTimeout !== null) {
animateTimeout = setTimeout(doAnimate, ANIMATE_INTERVAL)
}
})().catch(reportRejection)
}

function onPlay() {
console.log('Play button clicked')
if (animateTimeout) {
clearTimeout(animateTimeout)
animateTimeout = null
} else {
animateTimeout = setTimeout(doAnimate, ANIMATE_INTERVAL)
}
}

export const ModelingPaneHeader = ({
id,
icon,
Expand All @@ -40,6 +64,20 @@
)}
<span data-testid={id + '-header'}>{title}</span>
</div>
{id === 'code' && (
<ActionButton
Element="button"
iconStart={{
icon: 'play',
iconClassName: '!text-current',
bgClassName: 'bg-transparent dark:bg-transparent',
}}
className="!p-0 !bg-transparent hover:text-primary border-transparent dark:!border-transparent hover:!border-primary dark:hover:!border-chalkboard-70 !outline-none"
onClick={() => onPlay()}
>
<Tooltip position="bottom-right">Play</Tooltip>

Check warning on line 78 in src/components/ModelingSidebar/ModelingPane.tsx

View workflow job for this annotation

GitHub Actions / semgrep-oss/scan

jsx-not-internationalized

JSX element not internationalized Play. You should support different languages in your website or app with internationalization. Instead use packages such as i18next in order to internationalize your elements.
</ActionButton>
)}
{Menu instanceof Function ? <Menu /> : Menu}
<ActionButton
Element="button"
Expand Down
42 changes: 41 additions & 1 deletion src/lang/KclSingleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import {
compilationErrorsToDiagnostics,
kclErrorsToDiagnostics,
} from '@src/lang/errors'
import { executeAst, executeAstMock, lintAst } from '@src/lang/langHelpers'
import {
executeAdditional,
executeAst,
executeAstMock,
lintAst,
} from '@src/lang/langHelpers'
import { getNodeFromPath, getSettingsAnnotation } from '@src/lang/queryAst'
import { CommandLogType } from '@src/lang/std/commandLog'
import type { EngineCommandManager } from '@src/lang/std/engineConnection'
Expand Down Expand Up @@ -106,6 +111,7 @@ export class KclManager extends EventTarget {
preComments: [],
commentStart: 0,
}
private _animateState = { step: 0 }
private _execState: ExecState = emptyExecState()
private _variables: VariableMap = {}
lastSuccessfulVariables: VariableMap = {}
Expand Down Expand Up @@ -450,6 +456,7 @@ export class KclManager extends EventTarget {
const ast = args.ast || this.ast
markOnce('code/startExecuteAst')

this._animateState.step = 0
const currentExecutionId = args.executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)

Expand Down Expand Up @@ -541,6 +548,39 @@ export class KclManager extends EventTarget {
markOnce('code/endExecuteAst')
}

async executeAnimate(): Promise<void> {
if (this.isExecuting) {
return
}

const code = `animate(step = ${this._animateState.step})`
const result = parse(code)
if (err(result)) {
console.error(result)
return
}
const program = result.program
if (!program) {
console.error('No program returned from parse')
return
}

const { errors } = await executeAdditional({
ast: program,
path: this.singletons.codeManager.currentFilePath || undefined,
rustContext: this.singletons.rustContext,
})
if (errors.length > 0) {
console.error('Errors executing animate:', errors)
return
}

this._animateState.step += 1
if (this._animateState.step === Number.MAX_SAFE_INTEGER) {
this._animateState.step = 0
}
}

// DO NOT CALL THIS from codemirror ever.
async executeAstMock(ast: Program): Promise<null | Error> {
await this.ensureWasmInit()
Expand Down
25 changes: 25 additions & 0 deletions src/lang/langHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,31 @@ export async function executeAst({
}
}

export async function executeAdditional({
ast,
rustContext,
path,
}: {
ast: Node<Program>
rustContext: RustContext
path?: string
}): Promise<ExecutionResult> {
try {
const settings = await jsAppSettings()
const execState = await rustContext.executeAdditional(ast, settings, path)

await rustContext.waitForAllEngineCommands()
return {
logs: [],
errors: [],
execState,
isInterrupted: false,
}
} catch (e: any) {
return handleExecuteError(e)
}
}

export async function executeAstMock({
ast,
rustContext,
Expand Down
28 changes: 28 additions & 0 deletions src/lib/rustContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,34 @@ export default class RustContext {
}
}

/** Execute an additional program using the cache. */
async executeAdditional(
node: Node<Program>,
settings: DeepPartial<Configuration>,
path?: string
): Promise<ExecState> {
const instance = await this._checkInstance()

try {
const result = await instance.executeAdditional(
JSON.stringify(node),
path,
JSON.stringify(settings)
)
// Set the default planes, safe to call after execute.
const outcome = execStateFromRust(result)

this._defaultPlanes = outcome.defaultPlanes

// Return the result.
return outcome
} catch (e: any) {
const err = errFromErrWithOutputs(e)
this._defaultPlanes = err.defaultPlanes
return Promise.reject(err)
}
}

/** Execute a program with in mock mode. */
async executeMock(
node: Node<Program>,
Expand Down
Loading