Compare commits

...

1 Commits

Author SHA1 Message Date
bfefa0f51a Animate using a KCL function 2025-06-24 16:01:36 -04:00
8 changed files with 232 additions and 6 deletions

View File

@ -15,14 +15,14 @@ import "car-tire.kcl" as carTire
import * from "parameters.kcl" import * from "parameters.kcl"
// Place the car rotor // Place the car rotor
carRotor rotor = carRotor
|> translate(x = 0, y = 0.5, z = 0) |> translate(x = 0, y = 0.5, z = 0)
// Place the car wheel // Place the car wheel
carWheel carWheel
// Place the lug nuts // Place the lug nuts
lugNut lgnut = lugNut
|> patternCircular3d( |> patternCircular3d(
arcDegrees = 360, arcDegrees = 360,
axis = [0, 1, 0], axis = [0, 1, 0],
@ -32,8 +32,19 @@ lugNut
) )
// Place the brake caliper // Place the brake caliper
brakeCaliper cal = brakeCaliper
|> translate(x = 0, y = 0.5, z = 0) |> translate(x = 0, y = 0.5, z = 0)
// Place the car tire // Place the car tire
carTire 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
}

View File

@ -369,7 +369,7 @@ profile007 = startProfile(
|> line(%, endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(%, endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%) |> close(%)
profile008 = circle(sketch005, center = [0, 0], diameter = nubDiameter) profile008 = circle(sketch005, center = [0, 0], diameter = nubDiameter)
subtract2d(profile007, tool = profile008) hourHand = subtract2d(profile007, tool = profile008)
|> extrude(%, length = 5) |> extrude(%, length = 5)
|> appearance(%, color = "#404040") |> appearance(%, color = "#404040")
@ -413,7 +413,7 @@ profile009 = startProfile(
|> line(%, endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(%, endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%) |> close(%)
profile010 = circle(sketch006, center = [0, 0], diameter = 30) profile010 = circle(sketch006, center = [0, 0], diameter = 30)
subtract2d(profile009, tool = profile010) minuteHand = subtract2d(profile009, tool = profile010)
|> extrude(%, length = 5) |> extrude(%, length = 5)
|> appearance(%, color = "#404040") |> appearance(%, color = "#404040")
@ -439,3 +439,8 @@ profile004 = startProfile(sketch003, at = [-slotWidth / 2, 200])
|> extrude(%, length = -20) |> extrude(%, length = -20)
// todo: create cavity for the screw to slide into (need csg update) // 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)
}

View File

@ -805,6 +805,43 @@ impl ExecutorContext {
Ok(outcome) 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. /// Perform the execution of a program.
/// ///
/// To access non-fatal errors and warnings, extract them from the `ExecState`. /// To access non-fatal errors and warnings, extract them from the `ExecState`.

View File

@ -111,6 +111,48 @@ impl Context {
ctx.run_with_caching(program).await 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. /// Reset the scene and bust the cache.
/// ONLY use this if you absolutely need to 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)] #[wasm_bindgen(js_name = bustCacheAndResetScene)]

View File

@ -7,6 +7,8 @@ import type { CustomIconName } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip' import Tooltip from '@src/components/Tooltip'
import styles from './ModelingPane.module.css' import styles from './ModelingPane.module.css'
import { reportRejection } from '@src/lib/trap'
import { kclManager } from '@src/lib/singletons'
export interface ModelingPaneProps { export interface ModelingPaneProps {
id: string id: string
@ -19,6 +21,28 @@ export interface ModelingPaneProps {
onClose: () => void 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 = ({ export const ModelingPaneHeader = ({
id, id,
icon, icon,
@ -40,6 +64,20 @@ export const ModelingPaneHeader = ({
)} )}
<span data-testid={id + '-header'}>{title}</span> <span data-testid={id + '-header'}>{title}</span>
</div> </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>
</ActionButton>
)}
{Menu instanceof Function ? <Menu /> : Menu} {Menu instanceof Function ? <Menu /> : Menu}
<ActionButton <ActionButton
Element="button" Element="button"

View File

@ -17,7 +17,12 @@ import {
compilationErrorsToDiagnostics, compilationErrorsToDiagnostics,
kclErrorsToDiagnostics, kclErrorsToDiagnostics,
} from '@src/lang/errors' } 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 { getNodeFromPath, getSettingsAnnotation } from '@src/lang/queryAst'
import { CommandLogType } from '@src/lang/std/commandLog' import { CommandLogType } from '@src/lang/std/commandLog'
import type { EngineCommandManager } from '@src/lang/std/engineConnection' import type { EngineCommandManager } from '@src/lang/std/engineConnection'
@ -106,6 +111,7 @@ export class KclManager extends EventTarget {
preComments: [], preComments: [],
commentStart: 0, commentStart: 0,
} }
private _animateState = { step: 0 }
private _execState: ExecState = emptyExecState() private _execState: ExecState = emptyExecState()
private _variables: VariableMap = {} private _variables: VariableMap = {}
lastSuccessfulVariables: VariableMap = {} lastSuccessfulVariables: VariableMap = {}
@ -450,6 +456,7 @@ export class KclManager extends EventTarget {
const ast = args.ast || this.ast const ast = args.ast || this.ast
markOnce('code/startExecuteAst') markOnce('code/startExecuteAst')
this._animateState.step = 0
const currentExecutionId = args.executionId || Date.now() const currentExecutionId = args.executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false) this._cancelTokens.set(currentExecutionId, false)
@ -541,6 +548,39 @@ export class KclManager extends EventTarget {
markOnce('code/endExecuteAst') 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. // DO NOT CALL THIS from codemirror ever.
async executeAstMock(ast: Program): Promise<null | Error> { async executeAstMock(ast: Program): Promise<null | Error> {
await this.ensureWasmInit() await this.ensureWasmInit()

View File

@ -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({ export async function executeAstMock({
ast, ast,
rustContext, rustContext,

View File

@ -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. */ /** Execute a program with in mock mode. */
async executeMock( async executeMock(
node: Node<Program>, node: Node<Program>,