Merge branch 'main' into franknoirot/4088/create-file-url
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/workflows/build-and-store-wasm.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/build-and-store-wasm.yml
									
									
									
									
										vendored
									
									
								
							@ -27,7 +27,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      # Upload the WASM bundle as an artifact
 | 
			
		||||
      - uses: actions/upload-artifact@v3
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: wasm-bundle
 | 
			
		||||
          path: src/wasm-lib/pkg
 | 
			
		||||
 | 
			
		||||
@ -108,6 +108,8 @@ export class CameraControls {
 | 
			
		||||
  interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
 | 
			
		||||
  isFovAnimationInProgress = false
 | 
			
		||||
  perspectiveFovBeforeOrtho = 45
 | 
			
		||||
  // NOTE: Duplicated state across Provider and singleton. Mapped from settingsMachine
 | 
			
		||||
  _setting_allowOrbitInSketchMode = false
 | 
			
		||||
  get isPerspective() {
 | 
			
		||||
    return this.camera instanceof PerspectiveCamera
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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<VariableDeclaration>(
 | 
			
		||||
        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<VariableDeclaration>(
 | 
			
		||||
      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<VariableDeclaration>(
 | 
			
		||||
          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()
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ import { Command } from 'lib/commandTypes'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { CustomIcon } from './CustomIcon'
 | 
			
		||||
import { getActorNextEvents } from 'lib/utils'
 | 
			
		||||
import { sortCommands } from 'lib/commandUtils'
 | 
			
		||||
 | 
			
		||||
function CommandComboBox({
 | 
			
		||||
  options,
 | 
			
		||||
@ -19,8 +20,16 @@ function CommandComboBox({
 | 
			
		||||
 | 
			
		||||
  const defaultOption =
 | 
			
		||||
    options.find((o) => 'isCurrent' in o && o.isCurrent) || null
 | 
			
		||||
  // sort disabled commands to the bottom
 | 
			
		||||
  const sortedOptions = options
 | 
			
		||||
    .map((command) => ({
 | 
			
		||||
      command,
 | 
			
		||||
      disabled: optionIsDisabled(command),
 | 
			
		||||
    }))
 | 
			
		||||
    .sort(sortCommands)
 | 
			
		||||
    .map(({ command }) => command)
 | 
			
		||||
 | 
			
		||||
  const fuse = new Fuse(options, {
 | 
			
		||||
  const fuse = new Fuse(sortedOptions, {
 | 
			
		||||
    keys: ['displayName', 'name', 'description'],
 | 
			
		||||
    threshold: 0.3,
 | 
			
		||||
    ignoreLocation: true,
 | 
			
		||||
@ -28,7 +37,7 @@ function CommandComboBox({
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const results = fuse.search(query).map((result) => result.item)
 | 
			
		||||
    setFilteredOptions(query.length > 0 ? results : options)
 | 
			
		||||
    setFilteredOptions(query.length > 0 ? results : sortedOptions)
 | 
			
		||||
  }, [query])
 | 
			
		||||
 | 
			
		||||
  function handleSelection(command: Command) {
 | 
			
		||||
 | 
			
		||||
@ -111,7 +111,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
    auth,
 | 
			
		||||
    settings: {
 | 
			
		||||
      context: {
 | 
			
		||||
        app: { theme, enableSSAO },
 | 
			
		||||
        app: { theme, enableSSAO, allowOrbitInSketchMode },
 | 
			
		||||
        modeling: {
 | 
			
		||||
          defaultUnit,
 | 
			
		||||
          cameraProjection,
 | 
			
		||||
@ -121,6 +121,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  } = useSettingsAuthContext()
 | 
			
		||||
  const previousAllowOrbitInSketchMode = useRef(allowOrbitInSketchMode.current)
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const { context, send: fileMachineSend } = useFileContext()
 | 
			
		||||
  const { file } = useLoaderData() as IndexLoaderData
 | 
			
		||||
@ -634,7 +635,8 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            input.plane
 | 
			
		||||
          )
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, false)
 | 
			
		||||
          sceneInfra.camControls.enableRotate = false
 | 
			
		||||
          sceneInfra.camControls.enableRotate =
 | 
			
		||||
            sceneInfra.camControls._setting_allowOrbitInSketchMode
 | 
			
		||||
          sceneInfra.camControls.syncDirection = 'clientToEngine'
 | 
			
		||||
 | 
			
		||||
          await letEngineAnimateAndSyncCamAfter(
 | 
			
		||||
@ -647,6 +649,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            zAxis: input.zAxis,
 | 
			
		||||
            yAxis: input.yAxis,
 | 
			
		||||
            origin: [0, 0, 0],
 | 
			
		||||
            animateTargetId: input.planeId,
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
        'animate-to-sketch': fromPromise(
 | 
			
		||||
@ -671,6 +674,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
              origin: info.sketchDetails.origin.map(
 | 
			
		||||
                (a) => a / sceneInfra._baseUnitMultiplier
 | 
			
		||||
              ) as [number, number, number],
 | 
			
		||||
              animateTargetId: info?.sketchDetails?.faceId || '',
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
@ -1188,6 +1192,41 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
    }
 | 
			
		||||
  }, [engineCommandManager.engineConnection, modelingSend])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Only trigger this if the state actually changes, if it stays the same do not reload the camera
 | 
			
		||||
    if (
 | 
			
		||||
      previousAllowOrbitInSketchMode.current === allowOrbitInSketchMode.current
 | 
			
		||||
    ) {
 | 
			
		||||
      //no op
 | 
			
		||||
      previousAllowOrbitInSketchMode.current = allowOrbitInSketchMode.current
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const inSketchMode = modelingState.matches('Sketch')
 | 
			
		||||
 | 
			
		||||
    // If you are in sketch mode and you disable the orbit, return back to the normal view to the target
 | 
			
		||||
    if (!allowOrbitInSketchMode.current) {
 | 
			
		||||
      const targetId = modelingState.context.sketchDetails?.animateTargetId
 | 
			
		||||
      if (inSketchMode && targetId) {
 | 
			
		||||
        letEngineAnimateAndSyncCamAfter(engineCommandManager, targetId)
 | 
			
		||||
          .then(() => {})
 | 
			
		||||
          .catch((e) => {
 | 
			
		||||
            console.error(
 | 
			
		||||
              'failed to sync engine and client scene after disabling allow orbit in sketch mode'
 | 
			
		||||
            )
 | 
			
		||||
            console.error(e)
 | 
			
		||||
          })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // While you are in sketch mode you should be able to control the enable rotate
 | 
			
		||||
    // Once you exit it goes back to normal
 | 
			
		||||
    if (inSketchMode) {
 | 
			
		||||
      sceneInfra.camControls.enableRotate = allowOrbitInSketchMode.current
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    previousAllowOrbitInSketchMode.current = allowOrbitInSketchMode.current
 | 
			
		||||
  }, [allowOrbitInSketchMode])
 | 
			
		||||
 | 
			
		||||
  // Allow using the delete key to delete solids
 | 
			
		||||
  useHotkeys(['backspace', 'delete', 'del'], () => {
 | 
			
		||||
    modelingSend({ type: 'Delete selection' })
 | 
			
		||||
 | 
			
		||||
@ -137,6 +137,11 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
          sceneInfra.theme = opposingTheme
 | 
			
		||||
          sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
 | 
			
		||||
        },
 | 
			
		||||
        setAllowOrbitInSketchMode: ({ context }) => {
 | 
			
		||||
          sceneInfra.camControls._setting_allowOrbitInSketchMode =
 | 
			
		||||
            context.app.allowOrbitInSketchMode.current
 | 
			
		||||
          // ModelingMachineProvider will do a use effect to trigger the camera engine sync
 | 
			
		||||
        },
 | 
			
		||||
        toastSuccess: ({ event }) => {
 | 
			
		||||
          if (!('data' in event)) return
 | 
			
		||||
          const eventParts = event.type.replace(/^set./, '').split('.') as [
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										49
									
								
								src/lib/commandUtils.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/lib/commandUtils.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
import { CommandWithDisabledState, sortCommands } from './commandUtils'
 | 
			
		||||
 | 
			
		||||
function commandWithDisabled(
 | 
			
		||||
  name: string,
 | 
			
		||||
  disabled: boolean,
 | 
			
		||||
  groupId = 'modeling'
 | 
			
		||||
): CommandWithDisabledState {
 | 
			
		||||
  return {
 | 
			
		||||
    command: {
 | 
			
		||||
      name,
 | 
			
		||||
      groupId,
 | 
			
		||||
      needsReview: false,
 | 
			
		||||
      onSubmit: () => {},
 | 
			
		||||
    },
 | 
			
		||||
    disabled,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('Command sorting', () => {
 | 
			
		||||
  it(`Puts modeling commands first`, () => {
 | 
			
		||||
    const initial = [
 | 
			
		||||
      commandWithDisabled('a', false, 'settings'),
 | 
			
		||||
      commandWithDisabled('b', false, 'modeling'),
 | 
			
		||||
      commandWithDisabled('c', false, 'settings'),
 | 
			
		||||
    ]
 | 
			
		||||
    const sorted = initial.sort(sortCommands)
 | 
			
		||||
    expect(sorted[0].command.groupId).toBe('modeling')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it(`Puts disabled commands last`, () => {
 | 
			
		||||
    const initial = [
 | 
			
		||||
      commandWithDisabled('a', true, 'modeling'),
 | 
			
		||||
      commandWithDisabled('z', false, 'modeling'),
 | 
			
		||||
      commandWithDisabled('a', false, 'settings'),
 | 
			
		||||
    ]
 | 
			
		||||
    const sorted = initial.sort(sortCommands)
 | 
			
		||||
    expect(sorted[sorted.length - 1].disabled).toBe(true)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it(`Puts settings commands second to last`, () => {
 | 
			
		||||
    const initial = [
 | 
			
		||||
      commandWithDisabled('a', true, 'modeling'),
 | 
			
		||||
      commandWithDisabled('z', false, 'modeling'),
 | 
			
		||||
      commandWithDisabled('a', false, 'settings'),
 | 
			
		||||
    ]
 | 
			
		||||
    const sorted = initial.sort(sortCommands)
 | 
			
		||||
    expect(sorted[1].command.groupId).toBe('settings')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@ -2,6 +2,9 @@
 | 
			
		||||
// That object also contains some metadata about what to do with the KCL expression,
 | 
			
		||||
// such as whether we need to create a new variable for it.
 | 
			
		||||
// This function extracts the value field from those arg payloads and returns
 | 
			
		||||
 | 
			
		||||
import { Command } from './commandTypes'
 | 
			
		||||
 | 
			
		||||
// The arg object with all its field as natural values that the command to be executed will expect.
 | 
			
		||||
export function getCommandArgumentKclValuesOnly(args: Record<string, unknown>) {
 | 
			
		||||
  return Object.fromEntries(
 | 
			
		||||
@ -13,3 +16,42 @@ export function getCommandArgumentKclValuesOnly(args: Record<string, unknown>) {
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CommandWithDisabledState {
 | 
			
		||||
  command: Command
 | 
			
		||||
  disabled: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Sorting logic for commands in the command combo box.
 | 
			
		||||
 */
 | 
			
		||||
export function sortCommands(
 | 
			
		||||
  a: CommandWithDisabledState,
 | 
			
		||||
  b: CommandWithDisabledState
 | 
			
		||||
) {
 | 
			
		||||
  // Disabled commands should be at the bottom
 | 
			
		||||
  if (a.disabled && !b.disabled) {
 | 
			
		||||
    return 1
 | 
			
		||||
  }
 | 
			
		||||
  if (b.disabled && !a.disabled) {
 | 
			
		||||
    return -1
 | 
			
		||||
  }
 | 
			
		||||
  // Settings commands should be next-to-last
 | 
			
		||||
  if (a.command.groupId === 'settings' && b.command.groupId !== 'settings') {
 | 
			
		||||
    return 1
 | 
			
		||||
  }
 | 
			
		||||
  if (b.command.groupId === 'settings' && a.command.groupId !== 'settings') {
 | 
			
		||||
    return -1
 | 
			
		||||
  }
 | 
			
		||||
  // Modeling commands should be first
 | 
			
		||||
  if (a.command.groupId === 'modeling' && b.command.groupId !== 'modeling') {
 | 
			
		||||
    return -1
 | 
			
		||||
  }
 | 
			
		||||
  if (b.command.groupId === 'modeling' && a.command.groupId !== 'modeling') {
 | 
			
		||||
    return 1
 | 
			
		||||
  }
 | 
			
		||||
  // Sort alphabetically
 | 
			
		||||
  return (a.command.displayName || a.command.name).localeCompare(
 | 
			
		||||
    b.command.displayName || b.command.name
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -190,6 +190,14 @@ export function createSettings() {
 | 
			
		||||
          inputType: 'boolean',
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      allowOrbitInSketchMode: new Setting<boolean>({
 | 
			
		||||
        defaultValue: false,
 | 
			
		||||
        description: 'Toggle free camera while in sketch mode',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
        commandConfig: {
 | 
			
		||||
          inputType: 'boolean',
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      onboardingStatus: new Setting<OnboardingStatus>({
 | 
			
		||||
        defaultValue: '',
 | 
			
		||||
        // TODO: this could be better but we don't have a TS side real enum
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,8 @@ export function configurationToSettingsPayload(
 | 
			
		||||
      onboardingStatus: configuration?.settings?.app?.onboarding_status,
 | 
			
		||||
      dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
 | 
			
		||||
      streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
 | 
			
		||||
      allowOrbitInSketchMode:
 | 
			
		||||
        configuration?.settings?.app?.allow_orbit_in_sketch_mode,
 | 
			
		||||
      projectDirectory: configuration?.settings?.project?.directory,
 | 
			
		||||
      enableSSAO: configuration?.settings?.modeling?.enable_ssao,
 | 
			
		||||
    },
 | 
			
		||||
@ -80,6 +82,8 @@ export function projectConfigurationToSettingsPayload(
 | 
			
		||||
      onboardingStatus: configuration?.settings?.app?.onboarding_status,
 | 
			
		||||
      dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
 | 
			
		||||
      streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
 | 
			
		||||
      allowOrbitInSketchMode:
 | 
			
		||||
        configuration?.settings?.app?.allow_orbit_in_sketch_mode,
 | 
			
		||||
      enableSSAO: configuration?.settings?.modeling?.enable_ssao,
 | 
			
		||||
    },
 | 
			
		||||
    modeling: {
 | 
			
		||||
 | 
			
		||||
@ -460,18 +460,16 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
          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<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
            }),
 | 
			
		||||
          icon: 'circle',
 | 
			
		||||
          status: 'available',
 | 
			
		||||
          title: 'Three-point circle',
 | 
			
		||||
          title: '3-point circle',
 | 
			
		||||
          showTitle: false,
 | 
			
		||||
          description: 'Draw a circle defined by three points',
 | 
			
		||||
          links: [],
 | 
			
		||||
 | 
			
		||||
@ -133,6 +133,8 @@ export interface SketchDetails {
 | 
			
		||||
  zAxis: [number, number, number]
 | 
			
		||||
  yAxis: [number, number, number]
 | 
			
		||||
  origin: [number, number, number]
 | 
			
		||||
  // face id or plane id, both are strings
 | 
			
		||||
  animateTargetId?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SegmentOverlay {
 | 
			
		||||
@ -422,6 +424,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 +2191,10 @@ export const modelingMachine = setup({
 | 
			
		||||
              target: 'SketchIdle',
 | 
			
		||||
              guard: 'is editing existing sketch',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              target: 'circle3PointToolSelect',
 | 
			
		||||
              guard: 'is editing 3-point circle',
 | 
			
		||||
            },
 | 
			
		||||
            'Line tool',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
@ -2518,13 +2526,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 +2785,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<VariableDeclarator>(
 | 
			
		||||
    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 +2833,27 @@ export function pipeHasCircle({
 | 
			
		||||
  )
 | 
			
		||||
  return hasCircle
 | 
			
		||||
}
 | 
			
		||||
export function pipeHasCircleThreePoint({
 | 
			
		||||
  sketchDetails,
 | 
			
		||||
}: {
 | 
			
		||||
  sketchDetails: SketchDetails | null
 | 
			
		||||
}): boolean {
 | 
			
		||||
  if (!sketchDetails?.sketchPathToNode) return false
 | 
			
		||||
  const variableDeclaration = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
    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,
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,7 @@ export const settingsMachine = setup({
 | 
			
		||||
    'Execute AST': () => {},
 | 
			
		||||
    toastSuccess: () => {},
 | 
			
		||||
    setClientSideSceneUnits: () => {},
 | 
			
		||||
    setAllowOrbitInSketchMode: () => {},
 | 
			
		||||
    persistSettings: () => {},
 | 
			
		||||
    resetSettings: assign(({ context, event }) => {
 | 
			
		||||
      if (!('level' in event)) return {}
 | 
			
		||||
@ -157,6 +158,15 @@ export const settingsMachine = setup({
 | 
			
		||||
          actions: ['setSettingAtLevel', 'toastSuccess'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'set.app.allowOrbitInSketchMode': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
          actions: [
 | 
			
		||||
            'setSettingAtLevel',
 | 
			
		||||
            'toastSuccess',
 | 
			
		||||
            'setAllowOrbitInSketchMode',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        'set.modeling.cameraProjection': {
 | 
			
		||||
          target: 'persisting settings',
 | 
			
		||||
 | 
			
		||||
@ -183,6 +193,7 @@ export const settingsMachine = setup({
 | 
			
		||||
            'setClientSideSceneUnits',
 | 
			
		||||
            'Execute AST',
 | 
			
		||||
            'setClientTheme',
 | 
			
		||||
            'setAllowOrbitInSketchMode',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -194,6 +205,7 @@ export const settingsMachine = setup({
 | 
			
		||||
            'setClientSideSceneUnits',
 | 
			
		||||
            'Execute AST',
 | 
			
		||||
            'setClientTheme',
 | 
			
		||||
            'setAllowOrbitInSketchMode',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -815,7 +815,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
 | 
			
		||||
                context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
            if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new(&ctx.settings)).await {
 | 
			
		||||
                    return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                        error: e,
 | 
			
		||||
                        filename: format!("{}{}", #fn_name, #index),
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,10 @@ mod test_examples_someFn {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "someFn", 0usize),
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,10 @@ mod test_examples_someFn {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "someFn", 0usize),
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,10 @@ mod test_examples_show {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "show", 0usize),
 | 
			
		||||
@ -69,7 +72,10 @@ mod test_examples_show {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "show", 1usize),
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,10 @@ mod test_examples_show {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "show", 0usize),
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,10 @@ mod test_examples_my_func {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "my_func", 0usize),
 | 
			
		||||
@ -70,7 +73,10 @@ mod test_examples_my_func {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "my_func", 1usize),
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,10 @@ mod test_examples_line_to {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "line_to", 0usize),
 | 
			
		||||
@ -70,7 +73,10 @@ mod test_examples_line_to {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "line_to", 1usize),
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,10 @@ mod test_examples_min {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "min", 0usize),
 | 
			
		||||
@ -69,7 +72,10 @@ mod test_examples_min {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "min", 1usize),
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,10 @@ mod test_examples_show {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "show", 0usize),
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,10 @@ mod test_examples_import {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "import", 0usize),
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,10 @@ mod test_examples_import {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "import", 0usize),
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,10 @@ mod test_examples_import {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "import", 0usize),
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,10 @@ mod test_examples_show {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "show", 0usize),
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,10 @@ mod test_examples_some_function {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new()).await {
 | 
			
		||||
        if let Err(e) = ctx
 | 
			
		||||
            .run(program.into(), &mut crate::ExecState::new(&ctx.settings))
 | 
			
		||||
            .await
 | 
			
		||||
        {
 | 
			
		||||
            return Err(miette::Report::new(crate::errors::Report {
 | 
			
		||||
                error: e,
 | 
			
		||||
                filename: format!("{}{}", "some_function", 0usize),
 | 
			
		||||
 | 
			
		||||
@ -164,7 +164,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    eprintln!("Executing {test_name}");
 | 
			
		||||
    let mut exec_state = ExecState::new();
 | 
			
		||||
    let mut exec_state = ExecState::new(&state.settings);
 | 
			
		||||
    // This is a shitty source range, I don't know what else to use for it though.
 | 
			
		||||
    // There's no actual KCL associated with this reset_scene call.
 | 
			
		||||
    if let Err(e) = state
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
 | 
			
		||||
    let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new(
 | 
			
		||||
        crate::conn_mock_core::EngineConnection::new(ref_result).await?,
 | 
			
		||||
    )));
 | 
			
		||||
    ctx.run(program.into(), &mut ExecState::new()).await?;
 | 
			
		||||
    ctx.run(program.into(), &mut ExecState::new(&ctx.settings)).await?;
 | 
			
		||||
 | 
			
		||||
    let result = result.lock().expect("mutex lock").clone();
 | 
			
		||||
    Ok(result)
 | 
			
		||||
 | 
			
		||||
@ -30,13 +30,16 @@ impl From<KclErrorWithOutputs> for ExecError {
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct ExecErrorWithState {
 | 
			
		||||
    pub error: ExecError,
 | 
			
		||||
    pub exec_state: crate::ExecState,
 | 
			
		||||
    pub exec_state: Option<crate::ExecState>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ExecErrorWithState {
 | 
			
		||||
    #[cfg_attr(target_arch = "wasm32", expect(dead_code))]
 | 
			
		||||
    pub fn new(error: ExecError, exec_state: crate::ExecState) -> Self {
 | 
			
		||||
        Self { error, exec_state }
 | 
			
		||||
        Self {
 | 
			
		||||
            error,
 | 
			
		||||
            exec_state: Some(exec_state),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -44,7 +47,7 @@ impl From<ExecError> for ExecErrorWithState {
 | 
			
		||||
    fn from(error: ExecError) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            error,
 | 
			
		||||
            exec_state: Default::default(),
 | 
			
		||||
            exec_state: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -53,7 +56,7 @@ impl From<ConnectionError> for ExecErrorWithState {
 | 
			
		||||
    fn from(error: ConnectionError) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            error: error.into(),
 | 
			
		||||
            exec_state: Default::default(),
 | 
			
		||||
            exec_state: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -601,10 +601,24 @@ impl TryFrom<NumericSuffix> for UnitLen {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
 | 
			
		||||
impl From<crate::UnitLength> for UnitLen {
 | 
			
		||||
    fn from(unit: crate::UnitLength) -> Self {
 | 
			
		||||
        match unit {
 | 
			
		||||
            crate::UnitLength::Cm => UnitLen::Cm,
 | 
			
		||||
            crate::UnitLength::Ft => UnitLen::Feet,
 | 
			
		||||
            crate::UnitLength::In => UnitLen::Inches,
 | 
			
		||||
            crate::UnitLength::M => UnitLen::M,
 | 
			
		||||
            crate::UnitLength::Mm => UnitLen::Mm,
 | 
			
		||||
            crate::UnitLength::Yd => UnitLen::Yards,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(tag = "type")]
 | 
			
		||||
pub enum UnitAngle {
 | 
			
		||||
    #[default]
 | 
			
		||||
    Degrees,
 | 
			
		||||
    Radians,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ type Point2D = kcmc::shared::Point2d<f64>;
 | 
			
		||||
type Point3D = kcmc::shared::Point3d<f64>;
 | 
			
		||||
 | 
			
		||||
pub use function_param::FunctionParam;
 | 
			
		||||
pub use kcl_value::{KclObjectFields, KclValue};
 | 
			
		||||
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
mod annotations;
 | 
			
		||||
@ -77,7 +77,7 @@ pub struct GlobalState {
 | 
			
		||||
    pub artifact_commands: Vec<ArtifactCommand>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq)]
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct ModuleState {
 | 
			
		||||
    /// Program variable bindings.
 | 
			
		||||
@ -115,21 +115,15 @@ pub struct ExecOutcome {
 | 
			
		||||
    pub artifact_commands: Vec<ArtifactCommand>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for ExecState {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self::new()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ExecState {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
    pub fn new(exec_settings: &ExecutorSettings) -> Self {
 | 
			
		||||
        ExecState {
 | 
			
		||||
            global: GlobalState::new(),
 | 
			
		||||
            mod_local: ModuleState::default(),
 | 
			
		||||
            mod_local: ModuleState::new(exec_settings),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn reset(&mut self) {
 | 
			
		||||
    fn reset(&mut self, exec_settings: &ExecutorSettings) {
 | 
			
		||||
        let mut id_generator = self.global.id_generator.clone();
 | 
			
		||||
        // We do not pop the ids, since we want to keep the same id generator.
 | 
			
		||||
        // This is for the front end to keep track of the ids.
 | 
			
		||||
@ -140,7 +134,7 @@ impl ExecState {
 | 
			
		||||
 | 
			
		||||
        *self = ExecState {
 | 
			
		||||
            global,
 | 
			
		||||
            mod_local: ModuleState::default(),
 | 
			
		||||
            mod_local: ModuleState::new(exec_settings),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -204,6 +198,14 @@ impl ExecState {
 | 
			
		||||
 | 
			
		||||
        Ok(id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn length_unit(&self) -> UnitLen {
 | 
			
		||||
        self.mod_local.settings.default_length_units
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn angle_unit(&self) -> UnitAngle {
 | 
			
		||||
        self.mod_local.settings.default_angle_units
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GlobalState {
 | 
			
		||||
@ -232,6 +234,23 @@ impl GlobalState {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ModuleState {
 | 
			
		||||
    fn new(exec_settings: &ExecutorSettings) -> Self {
 | 
			
		||||
        ModuleState {
 | 
			
		||||
            memory: Default::default(),
 | 
			
		||||
            dynamic_state: Default::default(),
 | 
			
		||||
            pipe_value: Default::default(),
 | 
			
		||||
            module_exports: Default::default(),
 | 
			
		||||
            import_stack: Default::default(),
 | 
			
		||||
            operations: Default::default(),
 | 
			
		||||
            settings: MetaSettings {
 | 
			
		||||
                default_length_units: exec_settings.units.into(),
 | 
			
		||||
                default_angle_units: Default::default(),
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
@ -240,15 +259,6 @@ pub struct MetaSettings {
 | 
			
		||||
    pub default_angle_units: kcl_value::UnitAngle,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for MetaSettings {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        MetaSettings {
 | 
			
		||||
            default_length_units: kcl_value::UnitLen::Mm,
 | 
			
		||||
            default_angle_units: kcl_value::UnitAngle::Degrees,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MetaSettings {
 | 
			
		||||
    fn update_from_annotation(&mut self, annotation: &NonCodeValue, source_range: SourceRange) -> Result<(), KclError> {
 | 
			
		||||
        let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?;
 | 
			
		||||
@ -1711,7 +1721,7 @@ pub struct ExecutorContext {
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
pub struct ExecutorSettings {
 | 
			
		||||
    /// The unit to use in modeling dimensions.
 | 
			
		||||
    /// The project-default unit to use in modeling dimensions.
 | 
			
		||||
    pub units: UnitLength,
 | 
			
		||||
    /// Highlight edges of 3D objects?
 | 
			
		||||
    pub highlight_edges: bool,
 | 
			
		||||
@ -2214,7 +2224,7 @@ impl ExecutorContext {
 | 
			
		||||
 | 
			
		||||
        if cache_result.clear_scene && !self.is_mock() {
 | 
			
		||||
            // Pop the execution state, since we are starting fresh.
 | 
			
		||||
            exec_state.reset();
 | 
			
		||||
            exec_state.reset(&self.settings);
 | 
			
		||||
 | 
			
		||||
            // We don't do this in mock mode since there is no engine connection
 | 
			
		||||
            // anyways and from the TS side we override memory and don't want to clear it.
 | 
			
		||||
@ -2457,7 +2467,7 @@ impl ExecutorContext {
 | 
			
		||||
 | 
			
		||||
        let mut local_state = ModuleState {
 | 
			
		||||
            import_stack: exec_state.mod_local.import_stack.clone(),
 | 
			
		||||
            ..Default::default()
 | 
			
		||||
            ..ModuleState::new(&self.settings)
 | 
			
		||||
        };
 | 
			
		||||
        local_state.import_stack.push(info.path.clone());
 | 
			
		||||
        std::mem::swap(&mut exec_state.mod_local, &mut local_state);
 | 
			
		||||
@ -2830,7 +2840,7 @@ mod tests {
 | 
			
		||||
            settings: Default::default(),
 | 
			
		||||
            context_type: ContextType::Mock,
 | 
			
		||||
        };
 | 
			
		||||
        let mut exec_state = ExecState::default();
 | 
			
		||||
        let mut exec_state = ExecState::new(&ctx.settings);
 | 
			
		||||
        ctx.run(program.clone().into(), &mut exec_state).await?;
 | 
			
		||||
 | 
			
		||||
        Ok((program, ctx, exec_state))
 | 
			
		||||
@ -3302,6 +3312,25 @@ let shape = layer() |> patternTransform(10, transform, %)
 | 
			
		||||
        assert_eq!(7.4, mem_get_json(exec_state.memory(), "thing").as_f64().unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_unit_default() {
 | 
			
		||||
        let ast = r#"const inMm = 25.4 * mm()
 | 
			
		||||
const inInches = 1.0 * inch()"#;
 | 
			
		||||
        let (_, _, exec_state) = parse_execute(ast).await.unwrap();
 | 
			
		||||
        assert_eq!(25.4, mem_get_json(exec_state.memory(), "inMm").as_f64().unwrap());
 | 
			
		||||
        assert_eq!(25.4, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_unit_overriden() {
 | 
			
		||||
        let ast = r#"@settings(defaultLengthUnit = inch)
 | 
			
		||||
const inMm = 25.4 * mm()
 | 
			
		||||
const inInches = 1.0 * inch()"#;
 | 
			
		||||
        let (_, _, exec_state) = parse_execute(ast).await.unwrap();
 | 
			
		||||
        assert_eq!(1.0, mem_get_json(exec_state.memory(), "inMm").as_f64().unwrap().round());
 | 
			
		||||
        assert_eq!(1.0, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_zero_param_fn() {
 | 
			
		||||
        let ast = r#"const sigmaAllow = 35000 // psi
 | 
			
		||||
@ -4045,7 +4074,7 @@ shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#;
 | 
			
		||||
            .unwrap();
 | 
			
		||||
        let old_program = crate::Program::parse_no_errs(code).unwrap();
 | 
			
		||||
        // Execute the program.
 | 
			
		||||
        let mut exec_state = Default::default();
 | 
			
		||||
        let mut exec_state = ExecState::new(&ctx.settings);
 | 
			
		||||
        let cache_info = crate::CacheInformation {
 | 
			
		||||
            old: None,
 | 
			
		||||
            new_ast: old_program.ast.clone(),
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -49,7 +49,7 @@ use crate::{
 | 
			
		||||
        token::TokenStream,
 | 
			
		||||
        PIPE_OPERATOR,
 | 
			
		||||
    },
 | 
			
		||||
    CacheInformation, ModuleId, OldAstState, Program, SourceRange,
 | 
			
		||||
    CacheInformation, ExecState, ModuleId, OldAstState, Program, SourceRange,
 | 
			
		||||
};
 | 
			
		||||
const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [
 | 
			
		||||
    SemanticTokenType::NUMBER,
 | 
			
		||||
@ -693,7 +693,7 @@ impl Backend {
 | 
			
		||||
        let mut exec_state = if let Some(last_successful_ast_state) = last_successful_ast_state.clone() {
 | 
			
		||||
            last_successful_ast_state.exec_state
 | 
			
		||||
        } else {
 | 
			
		||||
            Default::default()
 | 
			
		||||
            ExecState::new(&executor_ctx.settings)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if let Err(err) = executor_ctx
 | 
			
		||||
 | 
			
		||||
@ -121,6 +121,9 @@ pub struct AppSettings {
 | 
			
		||||
    /// When the user is idle, and this is true, the stream will be torn down.
 | 
			
		||||
    #[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")]
 | 
			
		||||
    stream_idle_mode: bool,
 | 
			
		||||
    /// When the user is idle, and this is true, the stream will be torn down.
 | 
			
		||||
    #[serde(default, alias = "allowOrbitInSketchMode", skip_serializing_if = "is_default")]
 | 
			
		||||
    allow_orbit_in_sketch_mode: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: When we remove backwards compatibility with the old settings file, we can remove this.
 | 
			
		||||
@ -586,6 +589,7 @@ textWrapping = true
 | 
			
		||||
                        dismiss_web_banner: false,
 | 
			
		||||
                        enable_ssao: None,
 | 
			
		||||
                        stream_idle_mode: false,
 | 
			
		||||
                        allow_orbit_in_sketch_mode: false,
 | 
			
		||||
                    },
 | 
			
		||||
                    modeling: ModelingSettings {
 | 
			
		||||
                        base_unit: UnitLength::In,
 | 
			
		||||
@ -647,6 +651,7 @@ includeSettings = false
 | 
			
		||||
                        dismiss_web_banner: false,
 | 
			
		||||
                        enable_ssao: None,
 | 
			
		||||
                        stream_idle_mode: false,
 | 
			
		||||
                        allow_orbit_in_sketch_mode: false,
 | 
			
		||||
                    },
 | 
			
		||||
                    modeling: ModelingSettings {
 | 
			
		||||
                        base_unit: UnitLength::Yd,
 | 
			
		||||
@ -713,6 +718,7 @@ defaultProjectName = "projects-$nnn"
 | 
			
		||||
                        dismiss_web_banner: false,
 | 
			
		||||
                        enable_ssao: None,
 | 
			
		||||
                        stream_idle_mode: false,
 | 
			
		||||
                        allow_orbit_in_sketch_mode: false,
 | 
			
		||||
                    },
 | 
			
		||||
                    modeling: ModelingSettings {
 | 
			
		||||
                        base_unit: UnitLength::Yd,
 | 
			
		||||
@ -791,6 +797,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
 | 
			
		||||
                        dismiss_web_banner: false,
 | 
			
		||||
                        enable_ssao: None,
 | 
			
		||||
                        stream_idle_mode: false,
 | 
			
		||||
                        allow_orbit_in_sketch_mode: false,
 | 
			
		||||
                    },
 | 
			
		||||
                    modeling: ModelingSettings {
 | 
			
		||||
                        base_unit: UnitLength::Mm,
 | 
			
		||||
 | 
			
		||||
@ -124,6 +124,7 @@ includeSettings = false
 | 
			
		||||
                        dismiss_web_banner: false,
 | 
			
		||||
                        enable_ssao: None,
 | 
			
		||||
                        stream_idle_mode: false,
 | 
			
		||||
                        allow_orbit_in_sketch_mode: false,
 | 
			
		||||
                    },
 | 
			
		||||
                    modeling: ModelingSettings {
 | 
			
		||||
                        base_unit: UnitLength::Yd,
 | 
			
		||||
 | 
			
		||||
@ -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)]
 | 
			
		||||
@ -92,25 +96,6 @@ impl Args {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[cfg(test)]
 | 
			
		||||
    pub(crate) async fn new_test_args() -> Result<Self> {
 | 
			
		||||
        use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            args: Vec::new(),
 | 
			
		||||
            kw_args: Default::default(),
 | 
			
		||||
            source_range: SourceRange::default(),
 | 
			
		||||
            ctx: ExecutorContext {
 | 
			
		||||
                engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),
 | 
			
		||||
                fs: Arc::new(crate::fs::FileManager::new()),
 | 
			
		||||
                stdlib: Arc::new(crate::std::StdLib::new()),
 | 
			
		||||
                settings: Default::default(),
 | 
			
		||||
                context_type: crate::execution::ContextType::Mock,
 | 
			
		||||
            },
 | 
			
		||||
            pipe_value: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get a keyword argument. If not set, returns None.
 | 
			
		||||
    pub(crate) fn get_kw_arg_opt<'a, T>(&'a self, label: &str) -> Option<T>
 | 
			
		||||
    where
 | 
			
		||||
 | 
			
		||||
@ -5,14 +5,13 @@ use derive_docs::stdlib;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    errors::KclError,
 | 
			
		||||
    execution::{ExecState, KclValue},
 | 
			
		||||
    settings::types::UnitLength,
 | 
			
		||||
    execution::{ExecState, KclValue, UnitLen},
 | 
			
		||||
    std::Args,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Millimeters conversion factor for current projects units.
 | 
			
		||||
pub async fn mm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_mm(&args)?;
 | 
			
		||||
pub async fn mm(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_mm(exec_state)?;
 | 
			
		||||
 | 
			
		||||
    Ok(args.make_user_val_from_f64(result))
 | 
			
		||||
}
 | 
			
		||||
@ -40,20 +39,20 @@ pub async fn mm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
 | 
			
		||||
    name = "mm",
 | 
			
		||||
    tags = ["units"],
 | 
			
		||||
}]
 | 
			
		||||
fn inner_mm(args: &Args) -> Result<f64, KclError> {
 | 
			
		||||
    match args.ctx.settings.units {
 | 
			
		||||
        UnitLength::Mm => Ok(1.0),
 | 
			
		||||
        UnitLength::In => Ok(measurements::Length::from_millimeters(1.0).as_inches()),
 | 
			
		||||
        UnitLength::Ft => Ok(measurements::Length::from_millimeters(1.0).as_feet()),
 | 
			
		||||
        UnitLength::M => Ok(measurements::Length::from_millimeters(1.0).as_meters()),
 | 
			
		||||
        UnitLength::Cm => Ok(measurements::Length::from_millimeters(1.0).as_centimeters()),
 | 
			
		||||
        UnitLength::Yd => Ok(measurements::Length::from_millimeters(1.0).as_yards()),
 | 
			
		||||
fn inner_mm(exec_state: &ExecState) -> Result<f64, KclError> {
 | 
			
		||||
    match exec_state.length_unit() {
 | 
			
		||||
        UnitLen::Mm => Ok(1.0),
 | 
			
		||||
        UnitLen::Inches => Ok(measurements::Length::from_millimeters(1.0).as_inches()),
 | 
			
		||||
        UnitLen::Feet => Ok(measurements::Length::from_millimeters(1.0).as_feet()),
 | 
			
		||||
        UnitLen::M => Ok(measurements::Length::from_millimeters(1.0).as_meters()),
 | 
			
		||||
        UnitLen::Cm => Ok(measurements::Length::from_millimeters(1.0).as_centimeters()),
 | 
			
		||||
        UnitLen::Yards => Ok(measurements::Length::from_millimeters(1.0).as_yards()),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Inches conversion factor for current projects units.
 | 
			
		||||
pub async fn inch(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_inch(&args)?;
 | 
			
		||||
pub async fn inch(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_inch(exec_state)?;
 | 
			
		||||
 | 
			
		||||
    Ok(args.make_user_val_from_f64(result))
 | 
			
		||||
}
 | 
			
		||||
@ -81,20 +80,20 @@ pub async fn inch(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
 | 
			
		||||
    name = "inch",
 | 
			
		||||
    tags = ["units"],
 | 
			
		||||
}]
 | 
			
		||||
fn inner_inch(args: &Args) -> Result<f64, KclError> {
 | 
			
		||||
    match args.ctx.settings.units {
 | 
			
		||||
        UnitLength::Mm => Ok(measurements::Length::from_inches(1.0).as_millimeters()),
 | 
			
		||||
        UnitLength::In => Ok(1.0),
 | 
			
		||||
        UnitLength::Ft => Ok(measurements::Length::from_inches(1.0).as_feet()),
 | 
			
		||||
        UnitLength::M => Ok(measurements::Length::from_inches(1.0).as_meters()),
 | 
			
		||||
        UnitLength::Cm => Ok(measurements::Length::from_inches(1.0).as_centimeters()),
 | 
			
		||||
        UnitLength::Yd => Ok(measurements::Length::from_inches(1.0).as_yards()),
 | 
			
		||||
fn inner_inch(exec_state: &ExecState) -> Result<f64, KclError> {
 | 
			
		||||
    match exec_state.length_unit() {
 | 
			
		||||
        UnitLen::Mm => Ok(measurements::Length::from_inches(1.0).as_millimeters()),
 | 
			
		||||
        UnitLen::Inches => Ok(1.0),
 | 
			
		||||
        UnitLen::Feet => Ok(measurements::Length::from_inches(1.0).as_feet()),
 | 
			
		||||
        UnitLen::M => Ok(measurements::Length::from_inches(1.0).as_meters()),
 | 
			
		||||
        UnitLen::Cm => Ok(measurements::Length::from_inches(1.0).as_centimeters()),
 | 
			
		||||
        UnitLen::Yards => Ok(measurements::Length::from_inches(1.0).as_yards()),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Feet conversion factor for current projects units.
 | 
			
		||||
pub async fn ft(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_ft(&args)?;
 | 
			
		||||
pub async fn ft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_ft(exec_state)?;
 | 
			
		||||
 | 
			
		||||
    Ok(args.make_user_val_from_f64(result))
 | 
			
		||||
}
 | 
			
		||||
@ -123,20 +122,20 @@ pub async fn ft(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
 | 
			
		||||
    name = "ft",
 | 
			
		||||
    tags = ["units"],
 | 
			
		||||
}]
 | 
			
		||||
fn inner_ft(args: &Args) -> Result<f64, KclError> {
 | 
			
		||||
    match args.ctx.settings.units {
 | 
			
		||||
        UnitLength::Mm => Ok(measurements::Length::from_feet(1.0).as_millimeters()),
 | 
			
		||||
        UnitLength::In => Ok(measurements::Length::from_feet(1.0).as_inches()),
 | 
			
		||||
        UnitLength::Ft => Ok(1.0),
 | 
			
		||||
        UnitLength::M => Ok(measurements::Length::from_feet(1.0).as_meters()),
 | 
			
		||||
        UnitLength::Cm => Ok(measurements::Length::from_feet(1.0).as_centimeters()),
 | 
			
		||||
        UnitLength::Yd => Ok(measurements::Length::from_feet(1.0).as_yards()),
 | 
			
		||||
fn inner_ft(exec_state: &ExecState) -> Result<f64, KclError> {
 | 
			
		||||
    match exec_state.length_unit() {
 | 
			
		||||
        UnitLen::Mm => Ok(measurements::Length::from_feet(1.0).as_millimeters()),
 | 
			
		||||
        UnitLen::Inches => Ok(measurements::Length::from_feet(1.0).as_inches()),
 | 
			
		||||
        UnitLen::Feet => Ok(1.0),
 | 
			
		||||
        UnitLen::M => Ok(measurements::Length::from_feet(1.0).as_meters()),
 | 
			
		||||
        UnitLen::Cm => Ok(measurements::Length::from_feet(1.0).as_centimeters()),
 | 
			
		||||
        UnitLen::Yards => Ok(measurements::Length::from_feet(1.0).as_yards()),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Meters conversion factor for current projects units.
 | 
			
		||||
pub async fn m(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_m(&args)?;
 | 
			
		||||
pub async fn m(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_m(exec_state)?;
 | 
			
		||||
 | 
			
		||||
    Ok(args.make_user_val_from_f64(result))
 | 
			
		||||
}
 | 
			
		||||
@ -165,20 +164,20 @@ pub async fn m(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclE
 | 
			
		||||
    name = "m",
 | 
			
		||||
    tags = ["units"],
 | 
			
		||||
}]
 | 
			
		||||
fn inner_m(args: &Args) -> Result<f64, KclError> {
 | 
			
		||||
    match args.ctx.settings.units {
 | 
			
		||||
        UnitLength::Mm => Ok(measurements::Length::from_meters(1.0).as_millimeters()),
 | 
			
		||||
        UnitLength::In => Ok(measurements::Length::from_meters(1.0).as_inches()),
 | 
			
		||||
        UnitLength::Ft => Ok(measurements::Length::from_meters(1.0).as_feet()),
 | 
			
		||||
        UnitLength::M => Ok(1.0),
 | 
			
		||||
        UnitLength::Cm => Ok(measurements::Length::from_meters(1.0).as_centimeters()),
 | 
			
		||||
        UnitLength::Yd => Ok(measurements::Length::from_meters(1.0).as_yards()),
 | 
			
		||||
fn inner_m(exec_state: &ExecState) -> Result<f64, KclError> {
 | 
			
		||||
    match exec_state.length_unit() {
 | 
			
		||||
        UnitLen::Mm => Ok(measurements::Length::from_meters(1.0).as_millimeters()),
 | 
			
		||||
        UnitLen::Inches => Ok(measurements::Length::from_meters(1.0).as_inches()),
 | 
			
		||||
        UnitLen::Feet => Ok(measurements::Length::from_meters(1.0).as_feet()),
 | 
			
		||||
        UnitLen::M => Ok(1.0),
 | 
			
		||||
        UnitLen::Cm => Ok(measurements::Length::from_meters(1.0).as_centimeters()),
 | 
			
		||||
        UnitLen::Yards => Ok(measurements::Length::from_meters(1.0).as_yards()),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Centimeters conversion factor for current projects units.
 | 
			
		||||
pub async fn cm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_cm(&args)?;
 | 
			
		||||
pub async fn cm(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_cm(exec_state)?;
 | 
			
		||||
 | 
			
		||||
    Ok(args.make_user_val_from_f64(result))
 | 
			
		||||
}
 | 
			
		||||
@ -207,20 +206,20 @@ pub async fn cm(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
 | 
			
		||||
    name = "cm",
 | 
			
		||||
    tags = ["units"],
 | 
			
		||||
}]
 | 
			
		||||
fn inner_cm(args: &Args) -> Result<f64, KclError> {
 | 
			
		||||
    match args.ctx.settings.units {
 | 
			
		||||
        UnitLength::Mm => Ok(measurements::Length::from_centimeters(1.0).as_millimeters()),
 | 
			
		||||
        UnitLength::In => Ok(measurements::Length::from_centimeters(1.0).as_inches()),
 | 
			
		||||
        UnitLength::Ft => Ok(measurements::Length::from_centimeters(1.0).as_feet()),
 | 
			
		||||
        UnitLength::M => Ok(measurements::Length::from_centimeters(1.0).as_meters()),
 | 
			
		||||
        UnitLength::Cm => Ok(1.0),
 | 
			
		||||
        UnitLength::Yd => Ok(measurements::Length::from_centimeters(1.0).as_yards()),
 | 
			
		||||
fn inner_cm(exec_state: &ExecState) -> Result<f64, KclError> {
 | 
			
		||||
    match exec_state.length_unit() {
 | 
			
		||||
        UnitLen::Mm => Ok(measurements::Length::from_centimeters(1.0).as_millimeters()),
 | 
			
		||||
        UnitLen::Inches => Ok(measurements::Length::from_centimeters(1.0).as_inches()),
 | 
			
		||||
        UnitLen::Feet => Ok(measurements::Length::from_centimeters(1.0).as_feet()),
 | 
			
		||||
        UnitLen::M => Ok(measurements::Length::from_centimeters(1.0).as_meters()),
 | 
			
		||||
        UnitLen::Cm => Ok(1.0),
 | 
			
		||||
        UnitLen::Yards => Ok(measurements::Length::from_centimeters(1.0).as_yards()),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Yards conversion factor for current projects units.
 | 
			
		||||
pub async fn yd(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_yd(&args)?;
 | 
			
		||||
pub async fn yd(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
 | 
			
		||||
    let result = inner_yd(exec_state)?;
 | 
			
		||||
 | 
			
		||||
    Ok(args.make_user_val_from_f64(result))
 | 
			
		||||
}
 | 
			
		||||
@ -249,92 +248,13 @@ pub async fn yd(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
 | 
			
		||||
    name = "yd",
 | 
			
		||||
    tags = ["units"],
 | 
			
		||||
}]
 | 
			
		||||
fn inner_yd(args: &Args) -> Result<f64, KclError> {
 | 
			
		||||
    match args.ctx.settings.units {
 | 
			
		||||
        UnitLength::Mm => Ok(measurements::Length::from_yards(1.0).as_millimeters()),
 | 
			
		||||
        UnitLength::In => Ok(measurements::Length::from_yards(1.0).as_inches()),
 | 
			
		||||
        UnitLength::Ft => Ok(measurements::Length::from_yards(1.0).as_feet()),
 | 
			
		||||
        UnitLength::M => Ok(measurements::Length::from_yards(1.0).as_meters()),
 | 
			
		||||
        UnitLength::Cm => Ok(measurements::Length::from_yards(1.0).as_centimeters()),
 | 
			
		||||
        UnitLength::Yd => Ok(1.0),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use pretty_assertions::assert_eq;
 | 
			
		||||
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_units_inner_mm() {
 | 
			
		||||
        let mut args = Args::new_test_args().await.unwrap();
 | 
			
		||||
        args.ctx.settings.units = UnitLength::Mm;
 | 
			
		||||
        let result = inner_mm(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 1.0);
 | 
			
		||||
 | 
			
		||||
        args.ctx.settings.units = UnitLength::In;
 | 
			
		||||
        let result = inner_mm(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 1.0 / 25.4);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_units_inner_inch() {
 | 
			
		||||
        let mut args = Args::new_test_args().await.unwrap();
 | 
			
		||||
        args.ctx.settings.units = UnitLength::In;
 | 
			
		||||
        let result = inner_inch(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 1.0);
 | 
			
		||||
 | 
			
		||||
        args.ctx.settings.units = UnitLength::Mm;
 | 
			
		||||
        let result = inner_inch(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 25.4);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_units_inner_ft() {
 | 
			
		||||
        let mut args = Args::new_test_args().await.unwrap();
 | 
			
		||||
        args.ctx.settings.units = UnitLength::Ft;
 | 
			
		||||
        let result = inner_ft(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 1.0);
 | 
			
		||||
 | 
			
		||||
        args.ctx.settings.units = UnitLength::Mm;
 | 
			
		||||
        let result = inner_ft(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 304.8);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_units_inner_m() {
 | 
			
		||||
        let mut args = Args::new_test_args().await.unwrap();
 | 
			
		||||
        args.ctx.settings.units = UnitLength::M;
 | 
			
		||||
        let result = inner_m(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 1.0);
 | 
			
		||||
 | 
			
		||||
        args.ctx.settings.units = UnitLength::Mm;
 | 
			
		||||
        let result = inner_m(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 1000.0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_units_inner_cm() {
 | 
			
		||||
        let mut args = Args::new_test_args().await.unwrap();
 | 
			
		||||
        args.ctx.settings.units = UnitLength::Cm;
 | 
			
		||||
        let result = inner_cm(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 1.0);
 | 
			
		||||
 | 
			
		||||
        args.ctx.settings.units = UnitLength::Mm;
 | 
			
		||||
        let result = inner_cm(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 10.0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_units_inner_yd() {
 | 
			
		||||
        let mut args = Args::new_test_args().await.unwrap();
 | 
			
		||||
        args.ctx.settings.units = UnitLength::Yd;
 | 
			
		||||
        let result = inner_yd(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 1.0);
 | 
			
		||||
 | 
			
		||||
        args.ctx.settings.units = UnitLength::Mm;
 | 
			
		||||
        let result = inner_yd(&args).unwrap();
 | 
			
		||||
        assert_eq!(result, 914.4);
 | 
			
		||||
fn inner_yd(exec_state: &ExecState) -> Result<f64, KclError> {
 | 
			
		||||
    match exec_state.length_unit() {
 | 
			
		||||
        UnitLen::Mm => Ok(measurements::Length::from_yards(1.0).as_millimeters()),
 | 
			
		||||
        UnitLen::Inches => Ok(measurements::Length::from_yards(1.0).as_inches()),
 | 
			
		||||
        UnitLen::Feet => Ok(measurements::Length::from_yards(1.0).as_feet()),
 | 
			
		||||
        UnitLen::M => Ok(measurements::Length::from_yards(1.0).as_meters()),
 | 
			
		||||
        UnitLen::Cm => Ok(measurements::Length::from_yards(1.0).as_centimeters()),
 | 
			
		||||
        UnitLen::Yards => Ok(1.0),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ use crate::{
 | 
			
		||||
    errors::ExecErrorWithState,
 | 
			
		||||
    execution::{new_zoo_client, ArtifactCommand, ExecutorContext, ExecutorSettings, Operation, ProgramMemory},
 | 
			
		||||
    settings::types::UnitLength,
 | 
			
		||||
    ConnectionError, ExecError, KclErrorWithOutputs, Program,
 | 
			
		||||
    ConnectionError, ExecError, ExecState, KclErrorWithOutputs, Program,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Deserialize, serde::Serialize)]
 | 
			
		||||
@ -72,7 +72,7 @@ async fn do_execute_and_snapshot(
 | 
			
		||||
    ctx: &ExecutorContext,
 | 
			
		||||
    program: Program,
 | 
			
		||||
) -> Result<(crate::execution::ExecState, image::DynamicImage), ExecErrorWithState> {
 | 
			
		||||
    let mut exec_state = Default::default();
 | 
			
		||||
    let mut exec_state = ExecState::new(&ctx.settings);
 | 
			
		||||
    let snapshot_png_bytes = ctx
 | 
			
		||||
        .execute_and_prepare_snapshot(&program, &mut exec_state)
 | 
			
		||||
        .await
 | 
			
		||||
 | 
			
		||||
@ -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};
 | 
			
		||||
@ -80,7 +80,7 @@ pub async fn execute(
 | 
			
		||||
        kcl_lib::ExecutorContext::new(engine_manager, fs_manager, settings.into()).await?
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut exec_state = ExecState::default();
 | 
			
		||||
    let mut exec_state = ExecState::new(&ctx.settings);
 | 
			
		||||
    let mut old_ast_memory = None;
 | 
			
		||||
 | 
			
		||||
    // Populate from the old exec state if it exists.
 | 
			
		||||
@ -576,3 +576,26 @@ pub fn base64_decode(input: &str) -> Result<Vec<u8>, 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,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ async fn cache_test(
 | 
			
		||||
        .ok_or_else(|| anyhow::anyhow!("No variations provided for test '{}'", test_name))?;
 | 
			
		||||
 | 
			
		||||
    let mut ctx = kcl_lib::ExecutorContext::new_with_client(first.settings.clone(), None, None).await?;
 | 
			
		||||
    let mut exec_state = kcl_lib::ExecState::default();
 | 
			
		||||
    let mut exec_state = kcl_lib::ExecState::new(&ctx.settings);
 | 
			
		||||
 | 
			
		||||
    let mut old_ast_state = None;
 | 
			
		||||
    let mut img_results = Vec::new();
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ use pretty_assertions::assert_eq;
 | 
			
		||||
async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, ModuleId, uuid::Uuid)> {
 | 
			
		||||
    let program = Program::parse_no_errs(code)?;
 | 
			
		||||
    let ctx = kcl_lib::ExecutorContext::new_with_default_client(Default::default()).await?;
 | 
			
		||||
    let mut exec_state = ExecState::default();
 | 
			
		||||
    let mut exec_state = ExecState::new(&ctx.settings);
 | 
			
		||||
    ctx.run(program.clone().into(), &mut exec_state).await?;
 | 
			
		||||
 | 
			
		||||
    // We need to get the sketch ID.
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user