Skip to content

Commit 08830e0

Browse files
authored
KCL Python: Allow custom views, multiple screenshots of KCL (#7918)
This will allow teams to take multiple screenshots from different angles easily.
1 parent 72b6930 commit 08830e0

File tree

2 files changed

+122
-34
lines changed

2 files changed

+122
-34
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! For creating equivalents to the core kcmc types that support Python.
2+
use kittycad_modeling_cmds as kcmc;
3+
use pyo3::pyclass;
4+
use serde::{Deserialize, Serialize};
5+
6+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
7+
#[pyclass]
8+
pub struct Point3d {
9+
x: f32,
10+
y: f32,
11+
z: f32,
12+
}
13+
14+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
15+
#[pyclass]
16+
pub struct CameraLookAt {
17+
vantage: Point3d,
18+
center: Point3d,
19+
up: Point3d,
20+
}
21+
22+
impl From<CameraLookAt> for kcmc::DefaultCameraLookAt {
23+
fn from(CameraLookAt { vantage, center, up }: CameraLookAt) -> Self {
24+
Self {
25+
vantage: vantage.into(),
26+
center: center.into(),
27+
up: up.into(),
28+
sequence: None,
29+
}
30+
}
31+
}
32+
33+
impl From<Point3d> for kcmc::shared::Point3d<f32> {
34+
fn from(Point3d { x, y, z }: Point3d) -> Self {
35+
Self { x, y, z }
36+
}
37+
}

rust/kcl-python-bindings/src/lib.rs

Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ use kcl_lib::{
44
lint::{checks, Discovered},
55
ExecutorContext, UnitLength,
66
};
7+
use kittycad_modeling_cmds as kcmc;
78
use pyo3::{
89
prelude::PyModuleMethods, pyclass, pyfunction, pymethods, pymodule, types::PyModule, wrap_pyfunction, Bound, PyErr,
910
PyResult,
1011
};
1112
use serde::{Deserialize, Serialize};
1213

14+
mod bridge;
15+
1316
fn tokio() -> &'static tokio::runtime::Runtime {
1417
use std::sync::OnceLock;
1518
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
@@ -415,6 +418,27 @@ async fn execute_and_snapshot(path: String, image_format: ImageFormat) -> PyResu
415418
/// Execute the kcl code and snapshot it in a specific format.
416419
#[pyfunction]
417420
async fn execute_code_and_snapshot(code: String, image_format: ImageFormat) -> PyResult<Vec<u8>> {
421+
let mut snaps = execute_code_and_snapshot_at_views(code, image_format, Vec::new()).await?;
422+
Ok(snaps.pop().unwrap())
423+
}
424+
425+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
426+
#[pyclass]
427+
struct SnapshotOptions {
428+
/// If none, will use isometric view.
429+
camera: Option<bridge::CameraLookAt>,
430+
padding: f32,
431+
}
432+
433+
/// Execute the kcl code and snapshot it in a specific format.
434+
/// Returns one image for each camera angle you provide.
435+
/// If you don't provide any camera angles, a default head-on camera angle will be used.
436+
#[pyfunction]
437+
async fn execute_code_and_snapshot_at_views(
438+
code: String,
439+
image_format: ImageFormat,
440+
snapshot_options: Vec<SnapshotOptions>,
441+
) -> PyResult<Vec<Vec<u8>>> {
418442
tokio()
419443
.spawn(async move {
420444
let program =
@@ -428,46 +452,72 @@ async fn execute_code_and_snapshot(code: String, image_format: ImageFormat) -> P
428452
.await
429453
.map_err(|err| into_miette(err, &code))?;
430454

431-
// Zoom to fit.
432-
ctx.engine
433-
.send_modeling_cmd(
434-
uuid::Uuid::new_v4(),
435-
kcl_lib::SourceRange::default(),
436-
&kittycad_modeling_cmds::ModelingCmd::ZoomToFit(kittycad_modeling_cmds::ZoomToFit {
437-
object_ids: Default::default(),
438-
padding: 0.1,
439-
animated: false,
440-
}),
441-
)
442-
.await?;
443-
444-
// Send a snapshot request to the engine.
445-
let resp = ctx
446-
.engine
447-
.send_modeling_cmd(
448-
uuid::Uuid::new_v4(),
449-
kcl_lib::SourceRange::default(),
450-
&kittycad_modeling_cmds::ModelingCmd::TakeSnapshot(kittycad_modeling_cmds::TakeSnapshot {
451-
format: image_format.into(),
452-
}),
453-
)
454-
.await?;
455-
456-
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Modeling {
457-
modeling_response: kittycad_modeling_cmds::ok_response::OkModelingCmdResponse::TakeSnapshot(data),
458-
} = resp
459-
else {
460-
return Err(pyo3::exceptions::PyException::new_err(format!(
461-
"Unexpected response from engine: {resp:?}"
462-
)));
463-
};
455+
if snapshot_options.is_empty() {
456+
let data_bytes = snapshot(&ctx, image_format, 0.1).await?;
457+
return Ok(vec![data_bytes]);
458+
}
464459

465-
Ok(data.contents.0)
460+
let mut snaps = Vec::with_capacity(snapshot_options.len());
461+
for pre_snap in snapshot_options {
462+
if let Some(camera) = pre_snap.camera {
463+
let view_cmd = kcmc::DefaultCameraLookAt::from(camera);
464+
let view_cmd = kcmc::ModelingCmd::DefaultCameraLookAt(view_cmd);
465+
ctx.engine
466+
.send_modeling_cmd(uuid::Uuid::new_v4(), Default::default(), &view_cmd)
467+
.await?;
468+
} else {
469+
let view_cmd = kcmc::ModelingCmd::ViewIsometric(kcmc::ViewIsometric { padding: 0.0 });
470+
ctx.engine
471+
.send_modeling_cmd(uuid::Uuid::new_v4(), Default::default(), &view_cmd)
472+
.await?;
473+
}
474+
let data_bytes = snapshot(&ctx, image_format, pre_snap.padding).await?;
475+
snaps.push(data_bytes);
476+
}
477+
Ok(snaps)
466478
})
467479
.await
468480
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
469481
}
470482

483+
async fn snapshot(ctx: &ExecutorContext, image_format: ImageFormat, padding: f32) -> PyResult<Vec<u8>> {
484+
// Zoom to fit.
485+
ctx.engine
486+
.send_modeling_cmd(
487+
uuid::Uuid::new_v4(),
488+
kcl_lib::SourceRange::default(),
489+
&kittycad_modeling_cmds::ModelingCmd::ZoomToFit(kittycad_modeling_cmds::ZoomToFit {
490+
object_ids: Default::default(),
491+
padding,
492+
animated: false,
493+
}),
494+
)
495+
.await?;
496+
497+
// Send a snapshot request to the engine.
498+
let resp = ctx
499+
.engine
500+
.send_modeling_cmd(
501+
uuid::Uuid::new_v4(),
502+
kcl_lib::SourceRange::default(),
503+
&kittycad_modeling_cmds::ModelingCmd::TakeSnapshot(kittycad_modeling_cmds::TakeSnapshot {
504+
format: image_format.into(),
505+
}),
506+
)
507+
.await?;
508+
509+
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Modeling {
510+
modeling_response: kittycad_modeling_cmds::ok_response::OkModelingCmdResponse::TakeSnapshot(data),
511+
} = resp
512+
else {
513+
return Err(pyo3::exceptions::PyException::new_err(format!(
514+
"Unexpected response from engine: {resp:?}",
515+
)));
516+
};
517+
518+
Ok(data.contents.0)
519+
}
520+
471521
/// Execute a kcl file and export it to a specific file format.
472522
#[pyfunction]
473523
async fn execute_and_export(path: String, export_format: FileExportFormat) -> PyResult<Vec<ExportFile>> {
@@ -617,6 +667,7 @@ fn kcl(m: &Bound<'_, PyModule>) -> PyResult<()> {
617667
m.add_function(wrap_pyfunction!(mock_execute_code, m)?)?;
618668
m.add_function(wrap_pyfunction!(execute_and_snapshot, m)?)?;
619669
m.add_function(wrap_pyfunction!(execute_code_and_snapshot, m)?)?;
670+
m.add_function(wrap_pyfunction!(execute_code_and_snapshot_at_views, m)?)?;
620671
m.add_function(wrap_pyfunction!(execute_and_export, m)?)?;
621672
m.add_function(wrap_pyfunction!(execute_code_and_export, m)?)?;
622673
m.add_function(wrap_pyfunction!(format, m)?)?;

0 commit comments

Comments
 (0)