@@ -359,6 +359,16 @@ async fn mock_execute(path: String) -> PyResult<bool> {
359
359
/// Execute a kcl file and snapshot it in a specific format.
360
360
#[ pyfunction]
361
361
async fn execute_and_snapshot ( path : String , image_format : ImageFormat ) -> PyResult < Vec < u8 > > {
362
+ let img = execute_and_snapshot_views ( path, image_format, Vec :: new ( ) ) . await ?. pop ( ) ;
363
+ Ok ( img. unwrap ( ) )
364
+ }
365
+
366
+ #[ pyfunction]
367
+ async fn execute_and_snapshot_views (
368
+ path : String ,
369
+ image_format : ImageFormat ,
370
+ snapshot_options : Vec < SnapshotOptions > ,
371
+ ) -> PyResult < Vec < Vec < u8 > > > {
362
372
tokio ( )
363
373
. spawn ( async move {
364
374
let ( code, path) = get_code_and_file_path ( & path)
@@ -375,41 +385,7 @@ async fn execute_and_snapshot(path: String, image_format: ImageFormat) -> PyResu
375
385
. await
376
386
. map_err ( |err| into_miette ( err, & code) ) ?;
377
387
378
- // Zoom to fit.
379
- ctx. engine
380
- . send_modeling_cmd (
381
- uuid:: Uuid :: new_v4 ( ) ,
382
- kcl_lib:: SourceRange :: default ( ) ,
383
- & kittycad_modeling_cmds:: ModelingCmd :: ZoomToFit ( kittycad_modeling_cmds:: ZoomToFit {
384
- object_ids : Default :: default ( ) ,
385
- padding : 0.1 ,
386
- animated : false ,
387
- } ) ,
388
- )
389
- . await ?;
390
-
391
- // Send a snapshot request to the engine.
392
- let resp = ctx
393
- . engine
394
- . send_modeling_cmd (
395
- uuid:: Uuid :: new_v4 ( ) ,
396
- kcl_lib:: SourceRange :: default ( ) ,
397
- & kittycad_modeling_cmds:: ModelingCmd :: TakeSnapshot ( kittycad_modeling_cmds:: TakeSnapshot {
398
- format : image_format. into ( ) ,
399
- } ) ,
400
- )
401
- . await ?;
402
-
403
- let kittycad_modeling_cmds:: websocket:: OkWebSocketResponseData :: Modeling {
404
- modeling_response : kittycad_modeling_cmds:: ok_response:: OkModelingCmdResponse :: TakeSnapshot ( data) ,
405
- } = resp
406
- else {
407
- return Err ( pyo3:: exceptions:: PyException :: new_err ( format ! (
408
- "Unexpected response from engine: {resp:?}"
409
- ) ) ) ;
410
- } ;
411
-
412
- Ok ( data. contents . 0 )
388
+ take_snaps ( & ctx, image_format, snapshot_options) . await
413
389
} )
414
390
. await
415
391
. map_err ( |err| pyo3:: exceptions:: PyException :: new_err ( err. to_string ( ) ) ) ?
@@ -418,23 +394,43 @@ async fn execute_and_snapshot(path: String, image_format: ImageFormat) -> PyResu
418
394
/// Execute the kcl code and snapshot it in a specific format.
419
395
#[ pyfunction]
420
396
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 ?;
397
+ let mut snaps = execute_code_and_snapshot_views ( code, image_format, Vec :: new ( ) ) . await ?;
422
398
Ok ( snaps. pop ( ) . unwrap ( ) )
423
399
}
424
400
401
+ /// Customize a snapshot.
425
402
#[ derive( Serialize , Deserialize , PartialEq , Debug , Clone ) ]
426
403
#[ pyclass]
427
- struct SnapshotOptions {
404
+ pub struct SnapshotOptions {
428
405
/// If none, will use isometric view.
429
- camera : Option < bridge:: CameraLookAt > ,
430
- padding : f32 ,
406
+ pub camera : Option < bridge:: CameraLookAt > ,
407
+ /// How much to pad the view frame by, as a fraction of the object(s) bounding box size.
408
+ /// Negative padding will crop the view of the object proportionally.
409
+ /// e.g. padding = 0.2 means the view will span 120% of the object(s) bounding box,
410
+ /// and padding = -0.2 means the view will span 80% of the object(s) bounding box.
411
+ pub padding : f32 ,
412
+ }
413
+
414
+ #[ pymethods]
415
+ impl SnapshotOptions {
416
+ #[ new]
417
+ /// Takes a kcl.CameraLookAt, and a padding number.
418
+ fn new ( camera : Option < bridge:: CameraLookAt > , padding : f32 ) -> Self {
419
+ Self { camera, padding }
420
+ }
421
+
422
+ #[ staticmethod]
423
+ /// Takes a padding number.
424
+ fn isometric_view ( padding : f32 ) -> Self {
425
+ Self :: new ( None , padding)
426
+ }
431
427
}
432
428
433
429
/// Execute the kcl code and snapshot it in a specific format.
434
430
/// Returns one image for each camera angle you provide.
435
431
/// If you don't provide any camera angles, a default head-on camera angle will be used.
436
432
#[ pyfunction]
437
- async fn execute_code_and_snapshot_at_views (
433
+ async fn execute_code_and_snapshot_views (
438
434
code : String ,
439
435
image_format : ImageFormat ,
440
436
snapshot_options : Vec < SnapshotOptions > ,
@@ -452,34 +448,42 @@ async fn execute_code_and_snapshot_at_views(
452
448
. await
453
449
. map_err ( |err| into_miette ( err, & code) ) ?;
454
450
455
- if snapshot_options. is_empty ( ) {
456
- let data_bytes = snapshot ( & ctx, image_format, 0.1 ) . await ?;
457
- return Ok ( vec ! [ data_bytes] ) ;
458
- }
459
-
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)
451
+ take_snaps ( & ctx, image_format, snapshot_options) . await
478
452
} )
479
453
. await
480
454
. map_err ( |err| pyo3:: exceptions:: PyException :: new_err ( err. to_string ( ) ) ) ?
481
455
}
482
456
457
+ async fn take_snaps (
458
+ ctx : & ExecutorContext ,
459
+ image_format : ImageFormat ,
460
+ snapshot_options : Vec < SnapshotOptions > ,
461
+ ) -> PyResult < Vec < Vec < u8 > > > {
462
+ if snapshot_options. is_empty ( ) {
463
+ let data_bytes = snapshot ( ctx, image_format, 0.1 ) . await ?;
464
+ return Ok ( vec ! [ data_bytes] ) ;
465
+ }
466
+
467
+ let mut snaps = Vec :: with_capacity ( snapshot_options. len ( ) ) ;
468
+ for pre_snap in snapshot_options {
469
+ if let Some ( camera) = pre_snap. camera {
470
+ let view_cmd = kcmc:: DefaultCameraLookAt :: from ( camera) ;
471
+ let view_cmd = kcmc:: ModelingCmd :: DefaultCameraLookAt ( view_cmd) ;
472
+ ctx. engine
473
+ . send_modeling_cmd ( uuid:: Uuid :: new_v4 ( ) , Default :: default ( ) , & view_cmd)
474
+ . await ?;
475
+ } else {
476
+ let view_cmd = kcmc:: ModelingCmd :: ViewIsometric ( kcmc:: ViewIsometric { padding : 0.0 } ) ;
477
+ ctx. engine
478
+ . send_modeling_cmd ( uuid:: Uuid :: new_v4 ( ) , Default :: default ( ) , & view_cmd)
479
+ . await ?;
480
+ }
481
+ let data_bytes = snapshot ( ctx, image_format, pre_snap. padding ) . await ?;
482
+ snaps. push ( data_bytes) ;
483
+ }
484
+ Ok ( snaps)
485
+ }
486
+
483
487
async fn snapshot ( ctx : & ExecutorContext , image_format : ImageFormat , padding : f32 ) -> PyResult < Vec < u8 > > {
484
488
// Zoom to fit.
485
489
ctx. engine
@@ -657,6 +661,9 @@ fn kcl(m: &Bound<'_, PyModule>) -> PyResult<()> {
657
661
m. add_class :: < FileExportFormat > ( ) ?;
658
662
m. add_class :: < UnitLength > ( ) ?;
659
663
m. add_class :: < Discovered > ( ) ?;
664
+ m. add_class :: < SnapshotOptions > ( ) ?;
665
+ m. add_class :: < bridge:: Point3d > ( ) ?;
666
+ m. add_class :: < bridge:: CameraLookAt > ( ) ?;
660
667
661
668
// Add our functions to the module.
662
669
m. add_function ( wrap_pyfunction ! ( parse, m) ?) ?;
@@ -666,8 +673,9 @@ fn kcl(m: &Bound<'_, PyModule>) -> PyResult<()> {
666
673
m. add_function ( wrap_pyfunction ! ( mock_execute, m) ?) ?;
667
674
m. add_function ( wrap_pyfunction ! ( mock_execute_code, m) ?) ?;
668
675
m. add_function ( wrap_pyfunction ! ( execute_and_snapshot, m) ?) ?;
676
+ m. add_function ( wrap_pyfunction ! ( execute_and_snapshot_views, m) ?) ?;
669
677
m. add_function ( wrap_pyfunction ! ( execute_code_and_snapshot, m) ?) ?;
670
- m. add_function ( wrap_pyfunction ! ( execute_code_and_snapshot_at_views , m) ?) ?;
678
+ m. add_function ( wrap_pyfunction ! ( execute_code_and_snapshot_views , m) ?) ?;
671
679
m. add_function ( wrap_pyfunction ! ( execute_and_export, m) ?) ?;
672
680
m. add_function ( wrap_pyfunction ! ( execute_code_and_export, m) ?) ?;
673
681
m. add_function ( wrap_pyfunction ! ( format, m) ?) ?;
0 commit comments