Merge branch 'main' into franknoirot/4088/create-file-url

This commit is contained in:
Frank Noirot
2025-01-16 14:12:20 -05:00
44 changed files with 778 additions and 357 deletions

View File

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

View File

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

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

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

View File

@ -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' })

View File

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

View 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')
})
})

View File

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

View File

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

View File

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

View File

@ -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: [],

View File

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

View File

@ -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',
],
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

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

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

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

View File

@ -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();

View File

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