diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index 8ea5f93cd..b5544f8a8 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -1,5 +1,6 @@ import { BoxGeometry, + Color, DoubleSide, Group, Intersection, @@ -59,6 +60,7 @@ import { resultIsOk, SourceRange, } from 'lang/wasm' +import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib' import { engineCommandManager, kclManager, @@ -70,7 +72,7 @@ import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { executeAst, ToolTip } from 'lang/langHelpers' import { createProfileStartHandle, - createArcGeometry, + createCircleGeometry, SegmentUtils, segmentUtils, } from './segments' @@ -109,6 +111,8 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' import { Point3d } from 'wasm-lib/kcl/bindings/Point3d' import { SegmentInputs } from 'lang/std/stdTypes' import { Node } from 'wasm-lib/kcl/bindings/Node' +import { LabeledArg } from 'wasm-lib/kcl/bindings/LabeledArg' +import { Literal } from 'wasm-lib/kcl/bindings/Literal' import { radToDeg } from 'three/src/math/MathUtils' import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph' @@ -1261,110 +1265,98 @@ export class SceneEntities { const groupOfDrafts = new Group() groupOfDrafts.name = 'circle-3-point-group' groupOfDrafts.position.copy(sketchOrigin) + // lee: I'm keeping this here as a developer gotchya: - // Do not reorient your surfaces to the intersection plane. Your points are - // already in 3D space, not 2D. If you intersect say XZ, you want the points - // to continue to live at the 3D intersection point, not be rotated to end - // up elsewhere! - // groupOfDrafts.setRotationFromQuaternion(orientation) + // If you use 3D points, do not rotate anything. + // If you use 2D points (easier to deal with, generally do this!), then + // rotate the group just like this! Remember to rotate other groups too! + groupOfDrafts.setRotationFromQuaternion(orientation) this.scene.add(groupOfDrafts) - const DRAFT_POINT_RADIUS = 6 + // How large the points on the circle will render as + const DRAFT_POINT_RADIUS = 10 // px - const createPoint = (center: Vector3): number => { + // The target of our dragging + let target: Object3D | undefined = undefined + + // The KCL this will generate. + const kclCircle3Point = parse(`circleThreePoint( + p1 = [0.0, 0.0], + p2 = [0.0, 0.0], + p3 = [0.0, 0.0], + )`) + + const createPoint = ( + center: Vector3, + opts?: { noInteraction?: boolean } + ): Mesh => { const geometry = new SphereGeometry(DRAFT_POINT_RADIUS) const color = getThemeColorForThreeJs(sceneInfra._theme) - const material = new MeshBasicMaterial({ color }) + + const material = new MeshBasicMaterial({ + color: opts?.noInteraction + ? sceneInfra._theme === 'light' + ? new Color(color).multiplyScalar(0.15) + : new Color(0x010101).multiplyScalar(2000) + : color, + }) const mesh = new Mesh(geometry, material) - mesh.userData = { type: CIRCLE_3_POINT_DRAFT_POINT } + mesh.userData = { + type: opts?.noInteraction ? 'ghost' : CIRCLE_3_POINT_DRAFT_POINT, + } + mesh.renderOrder = 1000 mesh.layers.set(SKETCH_LAYER) mesh.position.copy(center) mesh.scale.set(scale, scale, scale) mesh.renderOrder = 100 - groupOfDrafts.add(mesh) - - return mesh.id + return mesh } - const circle3Point = ( - points: Vector2[] - ): undefined | { center: Vector3; radius: number } => { - // A 3-point circle is undefined if it doesn't have 3 points :) - if (points.length !== 3) return undefined - - // y = (i/j)(x-h) + b - // i and j variables for the slopes - const i = [points[1].x - points[0].x, points[2].x - points[1].x] - const j = [points[1].y - points[0].y, points[2].y - points[1].y] - - // Our / threejs coordinate system affects this a lot. If you take this - // code into a different code base, you may have to adjust a/b to being - // -1/a/b, b/a, etc! In this case, a/-b did the trick. - const m = [i[0] / -j[0], i[1] / -j[1]] - - const h = [ - (points[0].x + points[1].x) / 2, - (points[1].x + points[2].x) / 2, - ] - const b = [ - (points[0].y + points[1].y) / 2, - (points[1].y + points[2].y) / 2, - ] - - // Algebraically derived - const x = (-m[0] * h[0] + b[0] - b[1] + m[1] * h[1]) / (m[1] - m[0]) - const y = m[0] * (x - h[0]) + b[0] - - const center = new Vector3(x, y, 0) - const radius = Math.sqrt((points[1].x - x) ** 2 + (points[1].y - y) ** 2) - - return { - center, - radius, - } - } - - // TO BE SHORT LIVED: unused function to draw the circle and lines. - // @ts-ignore - // eslint-disable-next-line - const createCircle3Point = (points: Vector2[]) => { - const circleParams = circle3Point(points) - - // A circle cannot be created for these points. - if (!circleParams) return + const createCircle3PointGraphic = async ( + points: Vector2[], + center: Vector2, + radius: number + ) => { + if ( + Number.isNaN(radius) || + Number.isNaN(center.x) || + Number.isNaN(center.y) + ) + return const color = getThemeColorForThreeJs(sceneInfra._theme) - const geometryCircle = createArcGeometry({ - center: [circleParams.center.x, circleParams.center.y], - radius: circleParams.radius, - startAngle: 0, - endAngle: Math.PI * 2, - ccw: true, - isDashed: true, - scale, + const lineCircle = createCircleGeometry({ + center: [center.x, center.y], + radius, + color, + isDashed: false, + scale: 1, }) - const materialCircle = new MeshBasicMaterial({ color }) + lineCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE } + lineCircle.layers.set(SKETCH_LAYER) + // devnote: it's a mistake to use these with EllipseCurve :) + // lineCircle.position.set(center.x, center.y, 0) + // lineCircle.scale.set(scale, scale, scale) if (groupCircle) groupOfDrafts.remove(groupCircle) groupCircle = new Group() groupCircle.renderOrder = 1 + groupCircle.add(lineCircle) - const meshCircle = new Mesh(geometryCircle, materialCircle) - meshCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE } - meshCircle.layers.set(SKETCH_LAYER) - meshCircle.position.set(circleParams.center.x, circleParams.center.y, 0) - meshCircle.scale.set(scale, scale, scale) - groupCircle.add(meshCircle) + const pointMesh = createPoint(new Vector3(center.x, center.y, 0), { + noInteraction: true, + }) + groupCircle.add(pointMesh) const geometryPolyLine = new BufferGeometry().setFromPoints([ - ...points, - points[0], + ...points.map((p) => new Vector3(p.x, p.y, 0)), + new Vector3(points[0].x, points[0].y, 0), ]) const materialPolyLine = new LineDashedMaterial({ color, - scale, + scale: 1 / scale, dashSize: 6, gapSize: 6, }) @@ -1375,13 +1367,146 @@ export class SceneEntities { groupOfDrafts.add(groupCircle) } - // The target of our dragging - let target: Object3D | undefined = undefined + const insertCircle3PointKclIntoAstSnapshot = ( + points: Vector2[] + ): Program => { + if (err(kclCircle3Point) || kclCircle3Point.program === null) + return kclManager.ast + if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement') + return kclManager.ast + if ( + kclCircle3Point.program.body[0].expression.type !== 'CallExpressionKw' + ) + return kclManager.ast + + const arg = (x: LabeledArg): Literal[] | undefined => { + if ( + 'arg' in x && + 'elements' in x.arg && + x.arg.type === 'ArrayExpression' + ) { + if (x.arg.elements.every((x) => x.type === 'Literal')) { + return x.arg.elements + } + } + return undefined + } + + const kclCircle3PointArgs = + kclCircle3Point.program.body[0].expression.arguments + + const arg0 = arg(kclCircle3PointArgs[0]) + if (!arg0) return kclManager.ast + arg0[0].value = points[0].x + arg0[0].raw = points[0].x.toString() + arg0[1].value = points[0].y + arg0[1].raw = points[0].y.toString() + + const arg1 = arg(kclCircle3PointArgs[1]) + if (!arg1) return kclManager.ast + arg1[0].value = points[1].x + arg1[0].raw = points[1].x.toString() + arg1[1].value = points[1].y + arg1[1].raw = points[1].y.toString() + + const arg2 = arg(kclCircle3PointArgs[2]) + if (!arg2) return kclManager.ast + arg2[0].value = points[2].x + arg2[0].raw = points[2].x.toString() + arg2[1].value = points[2].y + arg2[1].raw = points[2].y.toString() + + const astSnapshot = structuredClone(kclManager.ast) + const startSketchOnASTNode = getNodeFromPath( + astSnapshot, + startSketchOnASTNodePath, + 'VariableDeclaration' + ) + if (err(startSketchOnASTNode)) return astSnapshot + + // It's possible we're already dealing with a PipeExpression. + // Modify the current one. + if ( + startSketchOnASTNode.node.declaration.init.type === 'PipeExpression' && + startSketchOnASTNode.node.declaration.init.body[1].type === + 'CallExpressionKw' && + startSketchOnASTNode.node.declaration.init.body.length >= 2 + ) { + startSketchOnASTNode.node.declaration.init.body[1].arguments = + kclCircle3Point.program.body[0].expression.arguments + } else { + // Clone a new node based on the old, and replace the old with the new. + const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode) + startSketchOnASTNode.node.declaration.init = createPipeExpression([ + clonedStartSketchOnASTNode.node.declaration.init, + kclCircle3Point.program.body[0].expression, + ]) + } + + // Return the `Program` + return astSnapshot + } + + const updateCircle3Point = async (opts?: { execute?: true }) => { + const points_ = Array.from(points.values()) + const circleParams = calculate_circle_from_3_points( + points_[0].x, + points_[0].y, + points_[1].x, + points_[1].y, + points_[2].x, + points_[2].y + ) + + if (Number.isNaN(circleParams.radius)) return + + await createCircle3PointGraphic( + points_, + new Vector2(circleParams.center_x, circleParams.center_y), + circleParams.radius + ) + const astWithNewCode = insertCircle3PointKclIntoAstSnapshot(points_) + const codeAsString = recast(astWithNewCode) + if (err(codeAsString)) return + codeManager.updateCodeStateEditor(codeAsString) + } const cleanupFn = () => { this.scene.remove(groupOfDrafts) } + // The AST node we extracted earlier may already have a circleThreePoint! + // Use the points in the AST as starting points. + const astSnapshot = structuredClone(kclManager.ast) + const maybeVariableDeclaration = getNodeFromPath( + astSnapshot, + startSketchOnASTNodePath, + 'VariableDeclaration' + ) + if (err(maybeVariableDeclaration)) + return () => { + done() + } + + const maybeCallExpressionKw = maybeVariableDeclaration.node.declaration.init + if ( + maybeCallExpressionKw.type === 'PipeExpression' && + maybeCallExpressionKw.body[1].type === 'CallExpressionKw' && + maybeCallExpressionKw.body[1]?.callee.name === 'circleThreePoint' + ) { + maybeCallExpressionKw?.body[1].arguments + .map( + ({ arg }: any) => + new Vector2(arg.elements[0].value, arg.elements[1].value) + ) + .forEach((point: Vector2) => { + const pointMesh = createPoint(new Vector3(point.x, point.y, 0)) + groupOfDrafts.add(pointMesh) + points.set(pointMesh.id, point) + }) + void updateCircle3Point() + } + sceneInfra.setCallbacks({ async onDrag(args) { const draftPointsIntersected = args.intersects.filter( @@ -1397,8 +1522,18 @@ export class SceneEntities { // The user was off their mark! Missed the object to select. if (!target) return - target.position.copy(args.intersectionPoint.threeD) + target.position.copy( + new Vector3( + args.intersectionPoint.twoD.x, + args.intersectionPoint.twoD.y, + 0 + ) + ) points.set(target.id, args.intersectionPoint.twoD) + + if (points.size <= 2) return + + await updateCircle3Point() }, async onDragEnd(_args) { target = undefined @@ -1407,45 +1542,19 @@ export class SceneEntities { if (points.size >= 3) return if (!args.intersectionPoint) return - const id = createPoint(args.intersectionPoint.threeD) - points.set(id, args.intersectionPoint.twoD) - - if (points.size < 2) return - - // We've now got 3 points, let's create our circle! - const astSnapshot = structuredClone(kclManager.ast) - let nodeQueryResult - nodeQueryResult = getNodeFromPath( - astSnapshot, - startSketchOnASTNodePath, - 'VariableDeclaration' + const pointMesh = createPoint( + new Vector3( + args.intersectionPoint.twoD.x, + args.intersectionPoint.twoD.y, + 0 + ) ) - if (err(nodeQueryResult)) return Promise.reject(nodeQueryResult) - const startSketchOnASTNode = nodeQueryResult + groupOfDrafts.add(pointMesh) + points.set(pointMesh.id, args.intersectionPoint.twoD) - const circleParams = circle3Point(Array.from(points.values())) + if (points.size <= 2) return - if (!circleParams) return - - const kclCircle3Point = parse(`circle({ - center = [${circleParams.center.x}, ${circleParams.center.y}], - radius = ${circleParams.radius}, - }, %)`) - - if (err(kclCircle3Point) || kclCircle3Point.program === null) return - if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement') - return - - const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode) - startSketchOnASTNode.node.declaration.init = createPipeExpression([ - clonedStartSketchOnASTNode.node.declaration.init, - kclCircle3Point.program.body[0].expression, - ]) - - await kclManager.executeAstMock(astSnapshot) - await codeManager.updateEditorWithAstAndWriteToFile(astSnapshot) - - done() + await updateCircle3Point() }, }) diff --git a/src/clientSideScene/segments.ts b/src/clientSideScene/segments.ts index 02c985049..0a152a62b 100644 --- a/src/clientSideScene/segments.ts +++ b/src/clientSideScene/segments.ts @@ -9,6 +9,9 @@ import { ExtrudeGeometry, Group, LineCurve3, + LineBasicMaterial, + LineDashedMaterial, + Line, Mesh, MeshBasicMaterial, NormalBufferAttributes, @@ -1003,6 +1006,49 @@ export function createArcGeometry({ return geo } +// (lee) The above is much more complex than necessary. +// I've derived the new code from: +// https://threejs.org/docs/#api/en/extras/curves/EllipseCurve +// I'm not sure why it wasn't done like this in the first place? +// I don't touch the code above because it may break something else. +export function createCircleGeometry({ + center, + radius, + color, + isDashed = false, + scale = 1, +}: { + center: Coords2d + radius: number + color: number + isDashed?: boolean + scale?: number +}): Line { + const circle = new EllipseCurve( + center[0], + center[1], + radius, + radius, + 0, + Math.PI * 2, + true, + scale + ) + const points = circle.getPoints(75) // just enough points to not see edges. + const geometry = new BufferGeometry().setFromPoints(points) + const material = !isDashed + ? new LineBasicMaterial({ color }) + : new LineDashedMaterial({ + color, + scale, + dashSize: 6, + gapSize: 6, + }) + const line = new Line(geometry, material) + line.computeLineDistances() + return line +} + export function dashedStraight( from: Coords2d, to: Coords2d, diff --git a/src/lib/toolbar.ts b/src/lib/toolbar.ts index 2701fc672..171d857fb 100644 --- a/src/lib/toolbar.ts +++ b/src/lib/toolbar.ts @@ -460,18 +460,16 @@ export const toolbarConfig: Record = { disabled: (state) => state.matches('Sketch no face') || (!canRectangleOrCircleTool(state.context) && - !state.matches({ Sketch: 'Circle tool' })), - isActive: (state) => state.matches({ Sketch: 'Circle tool' }), + !state.matches({ Sketch: 'Circle tool' }) && + !state.matches({ Sketch: 'circle3PointToolSelect' })), + isActive: (state) => + state.matches({ Sketch: 'Circle tool' }) || + state.matches({ Sketch: 'circle3PointToolSelect' }), hotkey: (state) => state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', showTitle: false, description: 'Start drawing a circle from its center', - links: [ - { - label: 'GitHub issue', - url: 'https://github.com/KittyCAD/modeling-app/issues/1501', - }, - ], + links: [], }, { id: 'circle-three-points', @@ -488,7 +486,7 @@ export const toolbarConfig: Record = { }), icon: 'circle', status: 'available', - title: 'Three-point circle', + title: '3-point circle', showTitle: false, description: 'Draw a circle defined by three points', links: [], diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 14a6bee12..3d0115dce 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -422,6 +422,8 @@ export const modelingMachine = setup({ }, 'is editing existing sketch': ({ context: { sketchDetails } }) => isEditingExistingSketch({ sketchDetails }), + 'is editing 3-point circle': ({ context: { sketchDetails } }) => + isEditing3PointCircle({ sketchDetails }), 'Can make selection horizontal': ({ context: { selectionRanges } }) => { const info = horzVertInfo(selectionRanges, 'horizontal') if (trap(info)) return false @@ -2187,6 +2189,10 @@ export const modelingMachine = setup({ target: 'SketchIdle', guard: 'is editing existing sketch', }, + { + target: 'circle3PointToolSelect', + guard: 'is editing 3-point circle', + }, 'Line tool', ], }, @@ -2518,13 +2524,8 @@ export const modelingMachine = setup({ circle3PointToolSelect: { invoke: { id: 'actor-circle-3-point', - input: function ({ context, event }) { - // These are not really necessary but I believe they are needed - // to satisfy TypeScript type narrowing or undefined check. - if (event.type !== 'change tool') return - if (event.data?.tool !== 'circle3Points') return + input: function ({ context }) { if (!context.sketchDetails) return - return context.sketchDetails }, src: 'actorCircle3Point', @@ -2782,6 +2783,34 @@ export function isEditingExistingSketch({ ) return (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle } +export function isEditing3PointCircle({ + sketchDetails, +}: { + sketchDetails: SketchDetails | null +}): boolean { + if (!sketchDetails?.sketchPathToNode) return false + const variableDeclaration = getNodeFromPath( + kclManager.ast, + sketchDetails.sketchPathToNode, + 'VariableDeclarator' + ) + if (err(variableDeclaration)) return false + if (variableDeclaration.node.type !== 'VariableDeclarator') return false + const pipeExpression = variableDeclaration.node.init + if (pipeExpression.type !== 'PipeExpression') return false + const hasStartProfileAt = pipeExpression.body.some( + (item) => + item.type === 'CallExpression' && item.callee.name === 'startProfileAt' + ) + const hasCircle3Point = pipeExpression.body.some( + (item) => + item.type === 'CallExpressionKw' && + item.callee.name === 'circleThreePoint' + ) + return ( + (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle3Point + ) +} export function pipeHasCircle({ sketchDetails, }: { @@ -2802,6 +2831,27 @@ export function pipeHasCircle({ ) return hasCircle } +export function pipeHasCircleThreePoint({ + sketchDetails, +}: { + sketchDetails: SketchDetails | null +}): boolean { + if (!sketchDetails?.sketchPathToNode) return false + const variableDeclaration = getNodeFromPath( + kclManager.ast, + sketchDetails.sketchPathToNode, + 'VariableDeclarator' + ) + if (err(variableDeclaration)) return false + if (variableDeclaration.node.type !== 'VariableDeclarator') return false + const pipeExpression = variableDeclaration.node.init + if (pipeExpression.type !== 'PipeExpression') return false + const hasCircle = pipeExpression.body.some( + (item) => + item.type === 'CallExpression' && item.callee.name === 'circleThreePoint' + ) + return hasCircle +} export function canRectangleOrCircleTool({ sketchDetails, diff --git a/src/wasm-lib/kcl/src/lib.rs b/src/wasm-lib/kcl/src/lib.rs index 5821478e0..b46f0edef 100644 --- a/src/wasm-lib/kcl/src/lib.rs +++ b/src/wasm-lib/kcl/src/lib.rs @@ -70,7 +70,7 @@ mod settings; #[cfg(test)] mod simulation_tests; mod source_range; -mod std; +pub mod std; #[cfg(not(target_arch = "wasm32"))] pub mod test_server; mod thread; @@ -84,7 +84,7 @@ pub use engine::{EngineManager, ExecutionKind}; pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs}; pub use execution::{ cache::{CacheInformation, OldAstState}, - ExecState, ExecutorContext, ExecutorSettings, + ExecState, ExecutorContext, ExecutorSettings, Point2d, }; pub use lsp::{ copilot::Backend as CopilotLspBackend, diff --git a/src/wasm-lib/kcl/src/std/args.rs b/src/wasm-lib/kcl/src/std/args.rs index 38b4d9a95..eb54c045b 100644 --- a/src/wasm-lib/kcl/src/std/args.rs +++ b/src/wasm-lib/kcl/src/std/args.rs @@ -55,6 +55,10 @@ impl KwArgs { pub fn len(&self) -> usize { self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 } } + /// Are there no arguments? + pub fn is_empty(&self) -> bool { + self.labeled.len() == 0 && self.unlabeled.is_none() + } } #[derive(Debug, Clone)] diff --git a/src/wasm-lib/kcl/src/std/utils.rs b/src/wasm-lib/kcl/src/std/utils.rs index af3dfd93e..4d7aefd79 100644 --- a/src/wasm-lib/kcl/src/std/utils.rs +++ b/src/wasm-lib/kcl/src/std/utils.rs @@ -270,6 +270,19 @@ pub fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64 [x, y] } +pub struct CircleParams { + pub center: Point2d, + pub radius: f64, +} + +pub fn calculate_circle_from_3_points(points: [Point2d; 3]) -> CircleParams { + let center: Point2d = calculate_circle_center(points[0].into(), points[1].into(), points[2].into()).into(); + CircleParams { + center, + radius: distance(center, points[1]), + } +} + #[cfg(test)] mod tests { // Here you can bring your functions into scope diff --git a/src/wasm-lib/src/wasm.rs b/src/wasm-lib/src/wasm.rs index 9bd07762e..1ba100ebc 100644 --- a/src/wasm-lib/src/wasm.rs +++ b/src/wasm-lib/src/wasm.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use futures::stream::TryStreamExt; use gloo_utils::format::JsValueSerdeExt; use kcl_lib::{ - exec::IdGenerator, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Program, + exec::IdGenerator, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Point2d, Program, }; use tokio::sync::RwLock; use tower_lsp::{LspService, Server}; @@ -576,3 +576,26 @@ pub fn base64_decode(input: &str) -> Result, JsValue> { Err(JsValue::from_str("Invalid base64 encoding")) } + +#[wasm_bindgen] +pub struct WasmCircleParams { + pub center_x: f64, + pub center_y: f64, + pub radius: f64, +} + +/// Calculate a circle from 3 points. +#[wasm_bindgen] +pub fn calculate_circle_from_3_points(ax: f64, ay: f64, bx: f64, by: f64, cx: f64, cy: f64) -> WasmCircleParams { + let result = kcl_lib::std::utils::calculate_circle_from_3_points([ + Point2d { x: ax, y: ay }, + Point2d { x: bx, y: by }, + Point2d { x: cx, y: cy }, + ]); + + WasmCircleParams { + center_x: result.center.x, + center_y: result.center.y, + radius: result.radius, + } +}