@@ -4,12 +4,15 @@ use kcl_lib::{
4
4
lint:: { checks, Discovered } ,
5
5
ExecutorContext , UnitLength ,
6
6
} ;
7
+ use kittycad_modeling_cmds as kcmc;
7
8
use pyo3:: {
8
9
prelude:: PyModuleMethods , pyclass, pyfunction, pymethods, pymodule, types:: PyModule , wrap_pyfunction, Bound , PyErr ,
9
10
PyResult ,
10
11
} ;
11
12
use serde:: { Deserialize , Serialize } ;
12
13
14
+ mod bridge;
15
+
13
16
fn tokio ( ) -> & ' static tokio:: runtime:: Runtime {
14
17
use std:: sync:: OnceLock ;
15
18
static RT : OnceLock < tokio:: runtime:: Runtime > = OnceLock :: new ( ) ;
@@ -415,6 +418,27 @@ async fn execute_and_snapshot(path: String, image_format: ImageFormat) -> PyResu
415
418
/// Execute the kcl code and snapshot it in a specific format.
416
419
#[ pyfunction]
417
420
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 > > > {
418
442
tokio ( )
419
443
. spawn ( async move {
420
444
let program =
@@ -428,46 +452,72 @@ async fn execute_code_and_snapshot(code: String, image_format: ImageFormat) -> P
428
452
. await
429
453
. map_err ( |err| into_miette ( err, & code) ) ?;
430
454
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
+ }
464
459
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)
466
478
} )
467
479
. await
468
480
. map_err ( |err| pyo3:: exceptions:: PyException :: new_err ( err. to_string ( ) ) ) ?
469
481
}
470
482
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
+
471
521
/// Execute a kcl file and export it to a specific file format.
472
522
#[ pyfunction]
473
523
async fn execute_and_export ( path : String , export_format : FileExportFormat ) -> PyResult < Vec < ExportFile > > {
@@ -617,6 +667,7 @@ fn kcl(m: &Bound<'_, PyModule>) -> PyResult<()> {
617
667
m. add_function ( wrap_pyfunction ! ( mock_execute_code, m) ?) ?;
618
668
m. add_function ( wrap_pyfunction ! ( execute_and_snapshot, m) ?) ?;
619
669
m. add_function ( wrap_pyfunction ! ( execute_code_and_snapshot, m) ?) ?;
670
+ m. add_function ( wrap_pyfunction ! ( execute_code_and_snapshot_at_views, m) ?) ?;
620
671
m. add_function ( wrap_pyfunction ! ( execute_and_export, m) ?) ?;
621
672
m. add_function ( wrap_pyfunction ! ( execute_code_and_export, m) ?) ?;
622
673
m. add_function ( wrap_pyfunction ! ( format, m) ?) ?;
0 commit comments