Compare commits

...

11 Commits

Author SHA1 Message Date
f79f11fd82 wip 2025-01-14 17:36:03 -05:00
5ada05063b Make cargo clippy happy 2025-01-14 14:11:05 -05:00
aab4d91b8c Ensure efficiency of updating editor vs execution 2025-01-14 13:56:03 -05:00
cfe472c066 Fix circle button not being activated 2025-01-14 11:56:23 -05:00
e822fbe0e9 Make points easier to select (enlarge) 2025-01-14 11:56:23 -05:00
87f0da36d0 Dont use isNaN 2025-01-14 11:56:23 -05:00
7ba2781a4a Fix clippy 2025-01-14 11:56:23 -05:00
fd81086936 Address Jon's comments 2025-01-14 11:56:23 -05:00
f933ab4434 cargo fmt 2025-01-14 11:56:23 -05:00
aae09bcf0e Cargo fmt 2025-01-14 11:56:23 -05:00
80d853f1c3 Add dragging behavior to 3 point circle
Uses our talked about technique of calling Rust functions to calculate new
geometry coordinates and parameters. It works very well!

Need to have the modeling app show "edit sketch" still.
2025-01-14 11:56:21 -05:00
11 changed files with 488 additions and 266 deletions

View File

@ -1,21 +1,21 @@
[
{
"file": "80-20-rail.kcl",
"file": "main.kcl",
"title": "80/20 Rail",
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
},
{
"file": "a-parametric-bearing-pillow-block.kcl",
"file": "main.kcl",
"title": "A Parametric Bearing Pillow Block",
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
},
{
"file": "ball-bearing.kcl",
"file": "main.kcl",
"title": "Ball Bearing",
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
},
{
"file": "bracket.kcl",
"file": "main.kcl",
"title": "Shelf Bracket",
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
},
@ -25,148 +25,128 @@
"description": "Brake calipers are used to squeeze the brake pads against the rotor, causing larger and larger amounts of friction depending on how hard the brakes are pressed."
},
{
"file": "car-wheel.kcl",
"title": "Car Wheel",
"description": "A sports car wheel with a circular lug pattern and spokes."
},
{
"file": "car-wheel-assembly.kcl",
"title": "Car Wheel Assembly",
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
},
{
"file": "dodecahedron.kcl",
"file": "main.kcl",
"title": "Hollow Dodecahedron",
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
},
{
"file": "enclosure.kcl",
"file": "main.kcl",
"title": "Enclosure",
"description": "An enclosure body and sealing lid for storing items"
},
{
"file": "flange-with-patterns.kcl",
"file": "main.kcl",
"title": "Flange",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "flange-xy.kcl",
"file": "main.kcl",
"title": "Flange with XY coordinates",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
},
{
"file": "focusrite-scarlett-mounting-bracket.kcl",
"file": "main.kcl",
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
},
{
"file": "food-service-spatula.kcl",
"file": "main.kcl",
"title": "Food Service Spatula",
"description": "Use these spatulas for mixing, flipping, and scraping."
},
{
"file": "french-press.kcl",
"file": "main.kcl",
"title": "French Press",
"description": "A french press immersion coffee maker"
},
{
"file": "gear.kcl",
"file": "main.kcl",
"title": "Spur Gear",
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
},
{
"file": "gear-rack.kcl",
"file": "main.kcl",
"title": "100mm Gear Rack",
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
},
{
"file": "hex-nut.kcl",
"file": "main.kcl",
"title": "Hex nut",
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
},
{
"file": "i-beam.kcl",
"file": "main.kcl",
"title": "I-beam",
"description": "A structural metal beam with an I shaped cross section. Often used in construction"
},
{
"file": "kitt.kcl",
"file": "main.kcl",
"title": "Kitt",
"description": "The beloved KittyCAD mascot in a voxelized style."
},
{
"file": "lego.kcl",
"file": "main.kcl",
"title": "Lego Brick",
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
},
{
"file": "lug-nut.kcl",
"title": "Lug Nut",
"description": "lug Nuts are essential components used to create secure connections, whether for electrical purposes, like terminating wires or grounding, or for mechanical purposes, such as providing mounting points or reinforcing structural joints."
},
{
"file": "mounting-plate.kcl",
"file": "main.kcl",
"title": "Mounting Plate",
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
},
{
"file": "multi-axis-robot.kcl",
"title": "Robot Arm",
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
"file": "globals.kcl",
"title": "Global constants for the multi-axis robot",
"description": ""
},
{
"file": "pipe.kcl",
"file": "main.kcl",
"title": "Pipe",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "pipe-flange-assembly.kcl",
"file": "main.kcl",
"title": "Pipe and Flange Assembly",
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
},
{
"file": "pipe-with-bend.kcl",
"file": "main.kcl",
"title": "Pipe with bend",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
},
{
"file": "poopy-shoe.kcl",
"file": "main.kcl",
"title": "Poopy Shoe",
"description": "poop shute for bambu labs printer - optimized for printing."
},
{
"file": "router-template-cross-bar.kcl",
"file": "main.kcl",
"title": "Router template for a cross bar",
"description": "A guide for routing a notch into a cross bar."
},
{
"file": "router-template-slate.kcl",
"file": "main.kcl",
"title": "Router template for a slate",
"description": "A guide for routing a slate for a cross bar."
},
{
"file": "sheet-metal-bracket.kcl",
"file": "main.kcl",
"title": "Sheet Metal Bracket",
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
},
{
"file": "socket-head-cap-screw.kcl",
"file": "main.kcl",
"title": "Socket Head Cap Screw",
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
},
{
"file": "tire.kcl",
"title": "Tire",
"description": "A tire is a critical component of a vehicle that provides the necessary traction and grip between the car and the road. It supports the vehicle's weight and absorbs shocks from road irregularities."
"file": "antenna.kcl",
"title": "Antenna",
"description": ""
},
{
"file": "washer.kcl",
"file": "main.kcl",
"title": "Washer",
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
},
{
"file": "wheel-rotor.kcl",
"title": "Wheel rotor",
"description": "A component of a disc brake system. It provides a surface for brake pads to press against, generating the friction needed to slow or stop the vehicle."
}
]

View File

@ -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()
},
})

View File

@ -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,

View File

@ -1,79 +1,72 @@
import { assertParse, initPromise, programMemoryInit } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers'
// These unit tests makes web requests to a public github repository.
import path from 'node:path'
import fs from 'node:fs/promises'
import child_process from 'node:child_process'
// The purpose of these tests is to act as a first line of defense
// if something gets real screwy with our KCL ecosystem.
// THESE TESTS ONLY RUN UNDER A NODEJS ENVIRONMENT. They DO NOT
// test under our application.
const DIR_KCL_SAMPLES = 'kcl-samples'
const URL_GIT_KCL_SAMPLES = 'https://github.com/KittyCAD/kcl-samples.git'
interface KclSampleFile {
file: string
pathFromProjectDirectoryToFirstFile: string
title: string
filename: string
description: string
}
try {
await fs.rm(DIR_KCL_SAMPLES, { recursive: true })
} catch (e) {
console.log(e)
}
child_process.spawnSync('git', ['clone', URL_GIT_KCL_SAMPLES, DIR_KCL_SAMPLES])
let files = await fs.readdir(DIR_KCL_SAMPLES)
const manifestJsonStr = await fs.readFile(
path.resolve(DIR_KCL_SAMPLES, 'manifest.json'),
'utf-8'
)
const manifest = JSON.parse(manifestJsonStr)
process.chdir(DIR_KCL_SAMPLES)
beforeAll(async () => {
await initPromise
})
// Only used to actually fetch an older version of KCL code that will break in the parser.
/* eslint-disable @typescript-eslint/no-unused-vars */
async function getBrokenSampleCodeForLocalTesting() {
const result = await fetch(
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/5ccd04a1773ebdbfd02684057917ce5dbe0eaab3/80-20-rail.kcl'
)
const text = await result.text()
return text
}
afterAll(async () => {
try {
process.chdir('..')
await fs.rm(DIR_KCL_SAMPLES, { recursive: true })
} catch (e) {}
})
async function getKclSampleCodeFromGithub(file: string): Promise<string> {
const result = await fetch(
`https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/${file}/${file}.kcl`
)
const text = await result.text()
return text
}
async function getFileNamesFromManifestJSON(): Promise<KclSampleFile[]> {
const result = await fetch(
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/manifest.json'
)
const json = await result.json()
json.forEach((file: KclSampleFile) => {
const filenameWithoutExtension = file.file.split('.')[0]
file.filename = filenameWithoutExtension
})
return json
}
// Value to use across all tests!
let files: KclSampleFile[] = []
describe('Test KCL Samples from public Github repository', () => {
describe('When parsing source code', () => {
// THIS RUNS ACROSS OTHER TESTS!
it('should fetch files', async () => {
files = await getFileNamesFromManifestJSON()
})
// Run through all of the files in the manifest json. This will allow us to be automatically updated
// with the latest changes in github. We won't be hard coding the filenames
files.forEach((file: KclSampleFile) => {
it(`should parse ${file.filename} without errors`, async () => {
const code = await getKclSampleCodeFromGithub(file.filename)
assertParse(code)
}, 1000)
})
})
describe('when performing enginelessExecutor', () => {
it(
'should run through all the files',
async () => {
for (let i = 0; i < files.length; i++) {
const file: KclSampleFile = files[i]
const code = await getKclSampleCodeFromGithub(file.filename)
// The tests have to be sequential because we need to change directories
// to support `import` working properly.
describe.sequential('Test KCL Samples from public Github repository', () => {
describe.sequential('when performing enginelessExecutor', () => {
manifest.forEach((file: KclSampleFile) => {
it.sequential(
`should execute ${file.title} (${file.file}) successfully`,
async () => {
const [dirProject, fileKcl] =
file.pathFromProjectDirectoryToFirstFile.split('/')
process.chdir(dirProject)
const code = await fs.readFile(fileKcl, 'utf-8')
const ast = assertParse(code)
await enginelessExecutor(ast, programMemoryInit())
}
},
files.length * 1000
)
process.chdir('..')
},
files.length * 1000
)
})
})
})

View File

@ -1,5 +1,23 @@
import { isDesktop } from 'lib/isDesktop'
// Polyfill window.electron fs functions as needed when in a nodejs context
// (INTENDED FOR VITEST SHINANGANS)
if (window?.electron === undefined) {
;(async () => {
const fs = await import('node:fs/promises')
const path = await import('node:path')
Object.assign(window, {
electron: {
readFile: fs.readFile,
stat: fs.stat,
readdir: fs.readdir,
path,
process: {},
},
})
})().catch(console.error)
}
/// FileSystemManager is a class that provides a way to read files from the local file system.
/// It assumes that you are in a project since it is solely used by the std lib
/// when executing code.
@ -19,13 +37,9 @@ class FileSystemManager {
}
async readFile(path: string): Promise<Uint8Array> {
// Using local file system only works from desktop.
if (!isDesktop()) {
return Promise.reject(
new Error(
'This function can only be called from the desktop application'
)
)
// Using local file system only works from desktop and nodejs
if (!window?.electron?.readFile) {
return Promise.reject(new Error('No polyfill found for this function'))
}
return this.join(this.dir, path).then((filePath) => {
@ -35,12 +49,8 @@ class FileSystemManager {
async exists(path: string): Promise<boolean | void> {
// Using local file system only works from desktop.
if (!isDesktop()) {
return Promise.reject(
new Error(
'This function can only be called from the desktop application'
)
)
if (!window?.electron?.stat) {
return Promise.reject(new Error('No polyfill found for this function'))
}
return this.join(this.dir, path).then(async (file) => {
@ -57,12 +67,8 @@ class FileSystemManager {
async getAllFiles(path: string): Promise<string[] | void> {
// Using local file system only works from desktop.
if (!isDesktop()) {
return Promise.reject(
new Error(
'This function can only be called from the desktop application'
)
)
if (!window?.electron?.readdir) {
return Promise.reject(new Error('No polyfill found for this function'))
}
return this.join(this.dir, path).then((filepath) => {

View File

@ -447,18 +447,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',
@ -475,7 +473,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: [],

View File

@ -420,6 +420,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
@ -2153,6 +2155,10 @@ export const modelingMachine = setup({
target: 'SketchIdle',
guard: 'is editing existing sketch',
},
{
target: 'circle3PointToolSelect',
guard: 'is editing 3-point circle',
},
'Line tool',
],
},
@ -2484,13 +2490,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',
@ -2735,6 +2736,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,
}: {
@ -2755,6 +2784,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,

View File

@ -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,

View File

@ -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)]

View File

@ -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

View File

@ -5,7 +5,7 @@ use std::sync::Arc;
use futures::stream::TryStreamExt;
use gloo_utils::format::JsValueSerdeExt;
use kcl_lib::{
exec::IdGenerator, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Program,
exec::IdGenerator, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Point2d, Program,
};
use tokio::sync::RwLock;
use tower_lsp::{LspService, Server};
@ -576,3 +576,26 @@ pub fn base64_decode(input: &str) -> Result<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,
}
}