1use anyhow::Result;
4use kcmc::{
5 ModelingCmd, each_cmd as mcmd,
6 length_unit::LengthUnit,
7 shared::{Angle, Point2d as KPoint2d},
8};
9use kittycad_modeling_cmds::{self as kcmc, shared::PathSegment, units::UnitLength};
10use serde::Serialize;
11
12use super::{
13 args::TyF64,
14 utils::{point_to_len_unit, point_to_mm, point_to_typed, untype_point, untyped_point_to_mm},
15};
16use crate::{
17 SourceRange,
18 errors::{KclError, KclErrorDetails},
19 execution::{
20 BasePath, ExecState, GeoMeta, KclValue, ModelingCmdMeta, Path, ProfileClosed, Sketch, SketchSurface,
21 types::{RuntimeType, adjust_length},
22 },
23 parsing::ast::types::TagNode,
24 std::{
25 Args,
26 utils::{calculate_circle_center, distance},
27 },
28};
29
30#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
32#[ts(export)]
33#[serde(untagged)]
34pub enum SketchOrSurface {
35 SketchSurface(SketchSurface),
36 Sketch(Box<Sketch>),
37}
38
39impl SketchOrSurface {
40 pub fn into_sketch_surface(self) -> SketchSurface {
41 match self {
42 SketchOrSurface::SketchSurface(surface) => surface,
43 SketchOrSurface::Sketch(sketch) => sketch.on,
44 }
45 }
46}
47
48pub async fn rectangle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
50 let sketch_or_surface =
51 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
52 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
53 let corner = args.get_kw_arg_opt("corner", &RuntimeType::point2d(), exec_state)?;
54 let width: TyF64 = args.get_kw_arg("width", &RuntimeType::length(), exec_state)?;
55 let height: TyF64 = args.get_kw_arg("height", &RuntimeType::length(), exec_state)?;
56
57 inner_rectangle(sketch_or_surface, center, corner, width, height, exec_state, args)
58 .await
59 .map(Box::new)
60 .map(|value| KclValue::Sketch { value })
61}
62
63async fn inner_rectangle(
64 sketch_or_surface: SketchOrSurface,
65 center: Option<[TyF64; 2]>,
66 corner: Option<[TyF64; 2]>,
67 width: TyF64,
68 height: TyF64,
69 exec_state: &mut ExecState,
70 args: Args,
71) -> Result<Sketch, KclError> {
72 let sketch_surface = sketch_or_surface.into_sketch_surface();
73
74 let (ty, corner) = match (center, corner) {
76 (Some(center), None) => (
77 center[0].ty,
78 [center[0].n - width.n / 2.0, center[1].n - height.n / 2.0],
79 ),
80 (None, Some(corner)) => (corner[0].ty, [corner[0].n, corner[1].n]),
81 (None, None) => {
82 return Err(KclError::new_semantic(KclErrorDetails::new(
83 "You must supply either `corner` or `center` arguments, but not both".to_string(),
84 vec![args.source_range],
85 )));
86 }
87 (Some(_), Some(_)) => {
88 return Err(KclError::new_semantic(KclErrorDetails::new(
89 "You must supply either `corner` or `center` arguments, but not both".to_string(),
90 vec![args.source_range],
91 )));
92 }
93 };
94 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
95 let corner_t = [TyF64::new(corner[0], ty), TyF64::new(corner[1], ty)];
96
97 let sketch = crate::std::sketch::inner_start_profile(
99 sketch_surface,
100 corner_t,
101 None,
102 exec_state,
103 &args.ctx,
104 args.source_range,
105 )
106 .await?;
107 let sketch_id = sketch.id;
108 let deltas = [[width.n, 0.0], [0.0, height.n], [-width.n, 0.0], [0.0, -height.n]];
109 let ids = [
110 exec_state.next_uuid(),
111 exec_state.next_uuid(),
112 exec_state.next_uuid(),
113 exec_state.next_uuid(),
114 ];
115 for (id, delta) in ids.iter().copied().zip(deltas) {
116 exec_state
117 .batch_modeling_cmd(
118 ModelingCmdMeta::from_args_id(exec_state, &args, id),
119 ModelingCmd::from(
120 mcmd::ExtendPath::builder()
121 .path(sketch.id.into())
122 .segment(PathSegment::Line {
123 end: KPoint2d::from(untyped_point_to_mm(delta, units))
124 .with_z(0.0)
125 .map(LengthUnit),
126 relative: true,
127 })
128 .build(),
129 ),
130 )
131 .await?;
132 }
133 exec_state
134 .batch_modeling_cmd(
135 ModelingCmdMeta::from_args_id(exec_state, &args, sketch_id),
136 ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
137 )
138 .await?;
139
140 let mut new_sketch = sketch;
142 new_sketch.is_closed = ProfileClosed::Explicitly;
143 fn add(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
144 [a[0] + b[0], a[1] + b[1]]
145 }
146 let a = (corner, add(corner, deltas[0]));
147 let b = (a.1, add(a.1, deltas[1]));
148 let c = (b.1, add(b.1, deltas[2]));
149 let d = (c.1, add(c.1, deltas[3]));
150 for (id, (from, to)) in ids.into_iter().zip([a, b, c, d]) {
151 let current_path = Path::ToPoint {
152 base: BasePath {
153 from,
154 to,
155 tag: None,
156 units,
157 geo_meta: GeoMeta {
158 id,
159 metadata: args.source_range.into(),
160 },
161 },
162 };
163 new_sketch.paths.push(current_path);
164 }
165 Ok(new_sketch)
166}
167
168pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
170 let sketch_or_surface =
171 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
172 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
173 let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
174 let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
175 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
176
177 let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
178 Ok(KclValue::Sketch {
179 value: Box::new(sketch),
180 })
181}
182
183const POINT_ZERO_ZERO: [TyF64; 2] = [
184 TyF64::new(0.0, crate::exec::NumericType::mm()),
185 TyF64::new(0.0, crate::exec::NumericType::mm()),
186];
187
188async fn inner_circle(
189 sketch_or_surface: SketchOrSurface,
190 center: Option<[TyF64; 2]>,
191 radius: Option<TyF64>,
192 diameter: Option<TyF64>,
193 tag: Option<TagNode>,
194 exec_state: &mut ExecState,
195 args: Args,
196) -> Result<Sketch, KclError> {
197 let sketch_surface = sketch_or_surface.into_sketch_surface();
198 let center = center.unwrap_or(POINT_ZERO_ZERO);
199 let (center_u, ty) = untype_point(center.clone());
200 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
201
202 let radius = get_radius(radius, diameter, args.source_range)?;
203 let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
204 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
205
206 let sketch =
207 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
208 .await?;
209
210 let angle_start = Angle::zero();
211 let angle_end = Angle::turn();
212
213 let id = exec_state.next_uuid();
214
215 exec_state
216 .batch_modeling_cmd(
217 ModelingCmdMeta::from_args_id(exec_state, &args, id),
218 ModelingCmd::from(
219 mcmd::ExtendPath::builder()
220 .path(sketch.id.into())
221 .segment(PathSegment::Arc {
222 start: angle_start,
223 end: angle_end,
224 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
225 radius: LengthUnit(radius.to_mm()),
226 relative: false,
227 })
228 .build(),
229 ),
230 )
231 .await?;
232
233 let current_path = Path::Circle {
234 base: BasePath {
235 from,
236 to: from,
237 tag: tag.clone(),
238 units,
239 geo_meta: GeoMeta {
240 id,
241 metadata: args.source_range.into(),
242 },
243 },
244 radius: radius.to_length_units(units),
245 center: center_u,
246 ccw: angle_start < angle_end,
247 };
248
249 let mut new_sketch = sketch;
250 new_sketch.is_closed = ProfileClosed::Explicitly;
251 if let Some(tag) = &tag {
252 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
253 }
254
255 new_sketch.paths.push(current_path);
256
257 exec_state
258 .batch_modeling_cmd(
259 ModelingCmdMeta::from_args_id(exec_state, &args, id),
260 ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
261 )
262 .await?;
263
264 Ok(new_sketch)
265}
266
267pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
269 let sketch_or_surface =
270 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
271 let p1 = args.get_kw_arg("p1", &RuntimeType::point2d(), exec_state)?;
272 let p2 = args.get_kw_arg("p2", &RuntimeType::point2d(), exec_state)?;
273 let p3 = args.get_kw_arg("p3", &RuntimeType::point2d(), exec_state)?;
274 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
275
276 let sketch = inner_circle_three_point(sketch_or_surface, p1, p2, p3, tag, exec_state, args).await?;
277 Ok(KclValue::Sketch {
278 value: Box::new(sketch),
279 })
280}
281
282async fn inner_circle_three_point(
285 sketch_surface_or_group: SketchOrSurface,
286 p1: [TyF64; 2],
287 p2: [TyF64; 2],
288 p3: [TyF64; 2],
289 tag: Option<TagNode>,
290 exec_state: &mut ExecState,
291 args: Args,
292) -> Result<Sketch, KclError> {
293 let ty = p1[0].ty;
294 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
295
296 let p1 = point_to_len_unit(p1, units);
297 let p2 = point_to_len_unit(p2, units);
298 let p3 = point_to_len_unit(p3, units);
299
300 let center = calculate_circle_center(p1, p2, p3);
301 let radius = distance(center, p2);
303
304 let sketch_surface = sketch_surface_or_group.into_sketch_surface();
305
306 let from = [TyF64::new(center[0] + radius, ty), TyF64::new(center[1], ty)];
307 let sketch = crate::std::sketch::inner_start_profile(
308 sketch_surface,
309 from.clone(),
310 None,
311 exec_state,
312 &args.ctx,
313 args.source_range,
314 )
315 .await?;
316
317 let angle_start = Angle::zero();
318 let angle_end = Angle::turn();
319
320 let id = exec_state.next_uuid();
321
322 exec_state
323 .batch_modeling_cmd(
324 ModelingCmdMeta::from_args_id(exec_state, &args, id),
325 ModelingCmd::from(
326 mcmd::ExtendPath::builder()
327 .path(sketch.id.into())
328 .segment(PathSegment::Arc {
329 start: angle_start,
330 end: angle_end,
331 center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
332 radius: adjust_length(units, radius, UnitLength::Millimeters).0.into(),
333 relative: false,
334 })
335 .build(),
336 ),
337 )
338 .await?;
339
340 let current_path = Path::CircleThreePoint {
341 base: BasePath {
342 from: untype_point(from.clone()).0,
344 to: untype_point(from).0,
345 tag: tag.clone(),
346 units,
347 geo_meta: GeoMeta {
348 id,
349 metadata: args.source_range.into(),
350 },
351 },
352 p1,
353 p2,
354 p3,
355 };
356
357 let mut new_sketch = sketch;
358 new_sketch.is_closed = ProfileClosed::Explicitly;
359 if let Some(tag) = &tag {
360 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
361 }
362
363 new_sketch.paths.push(current_path);
364
365 exec_state
366 .batch_modeling_cmd(
367 ModelingCmdMeta::from_args_id(exec_state, &args, id),
368 ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
369 )
370 .await?;
371
372 Ok(new_sketch)
373}
374
375#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, Default)]
377#[ts(export)]
378#[serde(rename_all = "lowercase")]
379pub enum PolygonType {
380 #[default]
381 Inscribed,
382 Circumscribed,
383}
384
385pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
387 let sketch_or_surface =
388 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
389 let radius: TyF64 = args.get_kw_arg("radius", &RuntimeType::length(), exec_state)?;
390 let num_sides: TyF64 = args.get_kw_arg("numSides", &RuntimeType::count(), exec_state)?;
391 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
392 let inscribed = args.get_kw_arg_opt("inscribed", &RuntimeType::bool(), exec_state)?;
393
394 let sketch = inner_polygon(
395 sketch_or_surface,
396 radius,
397 num_sides.n as u64,
398 center,
399 inscribed,
400 exec_state,
401 args,
402 )
403 .await?;
404 Ok(KclValue::Sketch {
405 value: Box::new(sketch),
406 })
407}
408
409#[allow(clippy::too_many_arguments)]
410async fn inner_polygon(
411 sketch_surface_or_group: SketchOrSurface,
412 radius: TyF64,
413 num_sides: u64,
414 center: [TyF64; 2],
415 inscribed: Option<bool>,
416 exec_state: &mut ExecState,
417 args: Args,
418) -> Result<Sketch, KclError> {
419 if num_sides < 3 {
420 return Err(KclError::new_type(KclErrorDetails::new(
421 "Polygon must have at least 3 sides".to_string(),
422 vec![args.source_range],
423 )));
424 }
425
426 if radius.n <= 0.0 {
427 return Err(KclError::new_type(KclErrorDetails::new(
428 "Radius must be greater than 0".to_string(),
429 vec![args.source_range],
430 )));
431 }
432
433 let (sketch_surface, units) = match sketch_surface_or_group {
434 SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.as_length().unwrap_or(UnitLength::Millimeters)),
435 SketchOrSurface::Sketch(group) => (group.on, group.units),
436 };
437
438 let half_angle = std::f64::consts::PI / num_sides as f64;
439
440 let radius_to_vertices = if inscribed.unwrap_or(true) {
441 radius.n
443 } else {
444 radius.n / libm::cos(half_angle)
446 };
447
448 let angle_step = std::f64::consts::TAU / num_sides as f64;
449
450 let center_u = point_to_len_unit(center, units);
451
452 let vertices: Vec<[f64; 2]> = (0..num_sides)
453 .map(|i| {
454 let angle = angle_step * i as f64;
455 [
456 center_u[0] + radius_to_vertices * libm::cos(angle),
457 center_u[1] + radius_to_vertices * libm::sin(angle),
458 ]
459 })
460 .collect();
461
462 let mut sketch = crate::std::sketch::inner_start_profile(
463 sketch_surface,
464 point_to_typed(vertices[0], units),
465 None,
466 exec_state,
467 &args.ctx,
468 args.source_range,
469 )
470 .await?;
471
472 for vertex in vertices.iter().skip(1) {
474 let from = sketch.current_pen_position()?;
475 let id = exec_state.next_uuid();
476
477 exec_state
478 .batch_modeling_cmd(
479 ModelingCmdMeta::from_args_id(exec_state, &args, id),
480 ModelingCmd::from(
481 mcmd::ExtendPath::builder()
482 .path(sketch.id.into())
483 .segment(PathSegment::Line {
484 end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
485 .with_z(0.0)
486 .map(LengthUnit),
487 relative: false,
488 })
489 .build(),
490 ),
491 )
492 .await?;
493
494 let current_path = Path::ToPoint {
495 base: BasePath {
496 from: from.ignore_units(),
497 to: *vertex,
498 tag: None,
499 units: sketch.units,
500 geo_meta: GeoMeta {
501 id,
502 metadata: args.source_range.into(),
503 },
504 },
505 };
506
507 sketch.paths.push(current_path);
508 }
509
510 let from = sketch.current_pen_position()?;
512 let close_id = exec_state.next_uuid();
513
514 exec_state
515 .batch_modeling_cmd(
516 ModelingCmdMeta::from_args_id(exec_state, &args, close_id),
517 ModelingCmd::from(
518 mcmd::ExtendPath::builder()
519 .path(sketch.id.into())
520 .segment(PathSegment::Line {
521 end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
522 .with_z(0.0)
523 .map(LengthUnit),
524 relative: false,
525 })
526 .build(),
527 ),
528 )
529 .await?;
530
531 let current_path = Path::ToPoint {
532 base: BasePath {
533 from: from.ignore_units(),
534 to: vertices[0],
535 tag: None,
536 units: sketch.units,
537 geo_meta: GeoMeta {
538 id: close_id,
539 metadata: args.source_range.into(),
540 },
541 },
542 };
543
544 sketch.paths.push(current_path);
545 sketch.is_closed = ProfileClosed::Explicitly;
546
547 exec_state
548 .batch_modeling_cmd(
549 ModelingCmdMeta::from_args(exec_state, &args),
550 ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
551 )
552 .await?;
553
554 Ok(sketch)
555}
556
557pub async fn ellipse(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
559 let sketch_or_surface =
560 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
561 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
562 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
563 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
564 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
565 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
566
567 let sketch = inner_ellipse(
568 sketch_or_surface,
569 center,
570 major_radius,
571 major_axis,
572 minor_radius,
573 tag,
574 exec_state,
575 args,
576 )
577 .await?;
578 Ok(KclValue::Sketch {
579 value: Box::new(sketch),
580 })
581}
582
583#[allow(clippy::too_many_arguments)]
584async fn inner_ellipse(
585 sketch_surface_or_group: SketchOrSurface,
586 center: [TyF64; 2],
587 major_radius: Option<TyF64>,
588 major_axis: Option<[TyF64; 2]>,
589 minor_radius: TyF64,
590 tag: Option<TagNode>,
591 exec_state: &mut ExecState,
592 args: Args,
593) -> Result<Sketch, KclError> {
594 let sketch_surface = sketch_surface_or_group.into_sketch_surface();
595 let (center_u, ty) = untype_point(center.clone());
596 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
597
598 let major_axis = match (major_axis, major_radius) {
599 (Some(_), Some(_)) | (None, None) => {
600 return Err(KclError::new_type(KclErrorDetails::new(
601 "Provide either `majorAxis` or `majorRadius`.".to_string(),
602 vec![args.source_range],
603 )));
604 }
605 (Some(major_axis), None) => major_axis,
606 (None, Some(major_radius)) => [
607 major_radius.clone(),
608 TyF64 {
609 n: 0.0,
610 ty: major_radius.ty,
611 },
612 ],
613 };
614
615 let from = [
616 center_u[0] + major_axis[0].to_length_units(units),
617 center_u[1] + major_axis[1].to_length_units(units),
618 ];
619 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
620
621 let sketch =
622 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
623 .await?;
624
625 let angle_start = Angle::zero();
626 let angle_end = Angle::turn();
627
628 let id = exec_state.next_uuid();
629
630 let axis = KPoint2d::from(untyped_point_to_mm([major_axis[0].n, major_axis[1].n], units)).map(LengthUnit);
631 exec_state
632 .batch_modeling_cmd(
633 ModelingCmdMeta::from_args_id(exec_state, &args, id),
634 ModelingCmd::from(
635 mcmd::ExtendPath::builder()
636 .path(sketch.id.into())
637 .segment(PathSegment::Ellipse {
638 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
639 major_axis: axis,
640 minor_radius: LengthUnit(minor_radius.to_mm()),
641 start_angle: Angle::from_degrees(angle_start.to_degrees()),
642 end_angle: Angle::from_degrees(angle_end.to_degrees()),
643 })
644 .build(),
645 ),
646 )
647 .await?;
648
649 let current_path = Path::Ellipse {
650 base: BasePath {
651 from,
652 to: from,
653 tag: tag.clone(),
654 units,
655 geo_meta: GeoMeta {
656 id,
657 metadata: args.source_range.into(),
658 },
659 },
660 major_axis: major_axis.map(|x| x.to_length_units(units)),
661 minor_radius: minor_radius.to_length_units(units),
662 center: center_u,
663 ccw: angle_start < angle_end,
664 };
665
666 let mut new_sketch = sketch;
667 new_sketch.is_closed = ProfileClosed::Explicitly;
668 if let Some(tag) = &tag {
669 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
670 }
671
672 new_sketch.paths.push(current_path);
673
674 exec_state
675 .batch_modeling_cmd(
676 ModelingCmdMeta::from_args_id(exec_state, &args, id),
677 ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
678 )
679 .await?;
680
681 Ok(new_sketch)
682}
683
684pub(crate) fn get_radius(
685 radius: Option<TyF64>,
686 diameter: Option<TyF64>,
687 source_range: SourceRange,
688) -> Result<TyF64, KclError> {
689 get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
690}
691
692pub(crate) fn get_radius_labelled(
693 radius: Option<TyF64>,
694 diameter: Option<TyF64>,
695 source_range: SourceRange,
696 label_radius: &'static str,
697 label_diameter: &'static str,
698) -> Result<TyF64, KclError> {
699 match (radius, diameter) {
700 (Some(radius), None) => Ok(radius),
701 (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
702 (None, None) => Err(KclError::new_type(KclErrorDetails::new(
703 format!("This function needs either `{label_diameter}` or `{label_radius}`"),
704 vec![source_range],
705 ))),
706 (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
707 format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
708 vec![source_range],
709 ))),
710 }
711}