Compare commits

..

7 Commits

67 changed files with 1496 additions and 2106 deletions

View File

@ -8117,7 +8117,7 @@ test('Typing KCL errors induces a badge on the error logs pane button', async ({
await u.closeDebugPanel()
// Ensure no badge is present
const errorLogsButton = page.getByRole('button', { name: 'KCL Code pane' })
const errorLogsButton = page.getByRole('button', { name: 'KCL Errors pane' })
await expect(errorLogsButton).not.toContainText('notification')
// Delete a character to break the KCL

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.24.7",
"version": "0.24.6",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",

View File

@ -23,7 +23,6 @@ export default defineConfig({
reporter: [
[process.env.CI ? 'dot' : 'list'],
['json', { outputFile: './test-results/report.json' }],
['html'],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {

7
src-tauri/Cargo.lock generated
View File

@ -2626,7 +2626,6 @@ dependencies = [
"tower-lsp",
"ts-rs",
"url",
"urlencoding",
"uuid",
"validator",
"wasm-bindgen",
@ -6259,12 +6258,6 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlpattern"
version = "0.2.0"

View File

@ -80,5 +80,5 @@
}
},
"productName": "Zoo Modeling App",
"version": "0.24.7"
"version": "0.24.6"
}

View File

@ -95,16 +95,16 @@ export function App() {
})
const newCmdId = uuidv4()
if (state.matches('idle.showPlanes')) return
if (context.store?.buttonDownInStream !== undefined) return
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
if (context.store?.buttonDownInStream === undefined) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
}
return (

View File

@ -190,59 +190,49 @@ export function Toolbar({
maybeIconConfig[0].onClick(configCallbackProps)
}
>
<span
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
>
{maybeIconConfig[0].title}
</span>
<ToolbarItemContents
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
/>
</ActionButton>
<ToolbarItemTooltip
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
/>
</ActionButtonDropdown>
)
}
const itemConfig = maybeIconConfig
return (
<div className="relative" key={itemConfig.id}>
<ActionButton
Element="button"
key={itemConfig.id}
id={itemConfig.id}
data-testid={itemConfig.id}
iconStart={{
icon: itemConfig.icon,
className: iconClassName,
bgClassName: bgClassName,
}}
className={
'pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
buttonBorderClassName +
' ' +
buttonBgClassName +
(!itemConfig.showTitle ? ' !px-0' : '')
}
name={itemConfig.title}
aria-description={itemConfig.description}
aria-pressed={itemConfig.isActive}
disabled={
disableAllButtons ||
itemConfig.status !== 'available' ||
itemConfig.disabled
}
onClick={() => itemConfig.onClick(configCallbackProps)}
>
<span className={!itemConfig.showTitle ? 'sr-only' : ''}>
{itemConfig.title}
</span>
</ActionButton>
<ToolbarItemTooltip
<ActionButton
Element="button"
key={itemConfig.id}
id={itemConfig.id}
data-testid={itemConfig.id}
iconStart={{
icon: itemConfig.icon,
className: iconClassName,
bgClassName: bgClassName,
}}
className={
'pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
buttonBorderClassName +
' ' +
buttonBgClassName +
(!itemConfig.showTitle ? ' !px-0' : '')
}
name={itemConfig.title}
aria-description={itemConfig.description}
aria-pressed={itemConfig.isActive}
disabled={
disableAllButtons ||
itemConfig.status !== 'available' ||
itemConfig.disabled
}
onClick={() => itemConfig.onClick(configCallbackProps)}
>
<ToolbarItemContents
itemConfig={itemConfig}
configCallbackProps={configCallbackProps}
/>
</div>
</ActionButton>
)
})}
</ul>
@ -260,7 +250,7 @@ export function Toolbar({
* It contains a tooltip with the title, description, and links
* and a hotkey listener
*/
const ToolbarItemTooltip = memo(function ToolbarItemContents({
const ToolbarItemContents = memo(function ToolbarItemContents({
itemConfig,
configCallbackProps,
}: {
@ -282,69 +272,73 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
)
return (
<Tooltip
inert={false}
position="bottom"
wrapperClassName="!p-4 !pointer-events-auto"
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
>
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
<span
className={`text-sm flex-1 ${
itemConfig.status !== 'available'
? 'text-chalkboard-70 dark:text-chalkboard-40'
: ''
}`}
>
{itemConfig.title}
</span>
{itemConfig.status === 'available' && itemConfig.hotkey ? (
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
) : itemConfig.status === 'kcl-only' ? (
<>
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
KCL code only
</span>
<CustomIcon
name="code"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
) : (
itemConfig.status === 'unavailable' && (
<>
<span className={!itemConfig.showTitle ? 'sr-only' : ''}>
{itemConfig.title}
</span>
<Tooltip
position="bottom"
wrapperClassName="!p-4 !pointer-events-auto"
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
>
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
<span
className={`text-sm flex-1 ${
itemConfig.status !== 'available'
? 'text-chalkboard-70 dark:text-chalkboard-40'
: ''
}`}
>
{itemConfig.title}
</span>
{itemConfig.status === 'available' && itemConfig.hotkey ? (
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
) : itemConfig.status === 'kcl-only' ? (
<>
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
In development
KCL code only
</span>
<CustomIcon
name="lockClosed"
name="code"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
)
) : (
itemConfig.status === 'unavailable' && (
<>
<span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
In development
</span>
<CustomIcon
name="lockClosed"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40"
/>
</>
)
)}
</div>
<p className="px-2 text-ch font-sans">{itemConfig.description}</p>
{itemConfig.links.length > 0 && (
<>
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
<ul className="p-0 px-1 m-0 flex flex-col">
{itemConfig.links.map((link) => (
<li key={link.label} className="contents">
<a
href={link.url}
target="_blank"
rel="noreferrer"
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
>
<span className="flex-1">Open {link.label}</span>
<CustomIcon name="link" className="w-4 h-4" />
</a>
</li>
))}
</ul>
</>
)}
</div>
<p className="px-2 text-ch font-sans">{itemConfig.description}</p>
{itemConfig.links.length > 0 && (
<>
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
<ul className="p-0 px-1 m-0 flex flex-col">
{itemConfig.links.map((link) => (
<li key={link.label} className="contents">
<a
href={link.url}
target="_blank"
rel="noreferrer"
className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit"
>
<span className="flex-1">Open {link.label}</span>
<CustomIcon name="link" className="w-4 h-4" />
</a>
</li>
))}
</ul>
</>
)}
</Tooltip>
</Tooltip>
</>
)
})

View File

@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils'
import { isArray, throttle } from 'lib/utils'
import {
sceneInfra,
kclManager,
@ -20,13 +20,17 @@ import {
getParentGroup,
} from './sceneEntities'
import { SegmentOverlay, SketchDetails } from 'machines/modelingMachine'
import { findUsesOfTagInPipe, getNodeFromPath } from 'lang/queryAst'
import {
expectNodeOnPath,
findUsesOfTagInPipe,
getLastNodeFromPath,
getNodeFromPath,
} from 'lang/queryAst'
import {
CallExpression,
PathToNode,
Program,
SourceRange,
Value,
parse,
recast,
} from 'lang/wasm'
@ -102,7 +106,6 @@ export const ClientSideScene = ({
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
sceneInfra.setSend(send)
engineCommandManager.modelingSend = send
return () => {
canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove)
canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown)
@ -187,13 +190,12 @@ const Overlay = ({
let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
const _node1 = getNodeFromPath<CallExpression>(
const callExpression = expectNodeOnPath<CallExpression>(
kclManager.ast,
overlay.pathToNode,
'CallExpression'
)
if (err(_node1)) return
const callExpression = _node1.node
if (err(callExpression)) return
const constraints = getConstraintInfo(
callExpression,
@ -550,13 +552,13 @@ const ConstraintSymbol = ({
varNameMap[_type as LineInputsType]?.implicitConstraintDesc
const _node = useMemo(
() => getNodeFromPath<Value>(kclManager.ast, pathToNode),
() => getLastNodeFromPath(kclManager.ast, pathToNode),
[kclManager.ast, pathToNode]
)
if (err(_node)) return
const node = _node.node
const range: SourceRange = node ? [node.start, node.end] : [0, 0]
const range: SourceRange = !isArray(node) ? [node.start, node.end] : [0, 0]
if (_type === 'intersectionTag') return null

View File

@ -22,6 +22,9 @@ import {
import {
ARROWHEAD,
AXIS_GROUP,
DEFAULT_PLANES,
DefaultPlane,
defaultPlaneColor,
getSceneScale,
INTERSECTION_PLANE_LAYER,
OnClickCallbackArgs,
@ -58,7 +61,12 @@ import {
codeManager,
editorManager,
} from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import {
isNodeType,
expectNodeOnPath,
getNodeFromPath,
getNodePathFromSourceRange,
} from 'lang/queryAst'
import { executeAst } from 'lang/langHelpers'
import {
createArcGeometry,
@ -74,7 +82,13 @@ import {
changeSketchArguments,
updateStartProfileAtArgs,
} from 'lang/std/sketch'
import { isOverlap, normaliseAngle, roundOff, throttle } from 'lib/utils'
import {
isArray,
isOverlap,
normaliseAngle,
roundOff,
throttle,
} from 'lib/utils'
import {
addStartProfileAt,
createArrayExpression,
@ -199,7 +213,6 @@ export class SceneEntities {
createIntersectionPlane() {
if (sceneInfra.scene.getObjectByName(RAYCASTABLE_PLANE)) {
// this.removeIntersectionPlane()
console.warn('createIntersectionPlane called when it already exists')
return
}
@ -461,13 +474,18 @@ export class SceneEntities {
)
let seg: Group
const _node1 = getNodeFromPath<CallExpression>(
const callExp = getNodeFromPath<CallExpression>(
maybeModdedAst,
segPathToNode,
'CallExpression'
)
if (err(_node1)) return
const callExpName = _node1.node?.callee?.name
if (err(callExp)) return
const callExpName = isNodeType<CallExpression>(
callExp.node,
'CallExpression'
)
? callExp.node.callee.name
: ''
if (segment.type === 'TangentialArcTo') {
seg = tangentialArcToSegment({
@ -588,8 +606,12 @@ export class SceneEntities {
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName =
_node1.node?.declarations?.[0]?.id?.name || ''
const variableDeclarationName = isNodeType<VariableDeclaration>(
_node1.node,
'VariableDeclaration'
)
? _node1.node.declarations[0]?.id?.name || ''
: ''
const sg = kclManager.programMemory.get(
variableDeclarationName
@ -726,15 +748,14 @@ export class SceneEntities {
) => {
let _ast = structuredClone(kclManager.ast)
const _node1 = getNodeFromPath<VariableDeclaration>(
const varDec = expectNodeOnPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName =
_node1.node?.declarations?.[0]?.id?.name || ''
const startSketchOn = _node1.node?.declarations
if (trap(varDec)) return Promise.reject(varDec)
const variableDeclarationName = varDec.declarations?.[0]?.id?.name || ''
const startSketchOn = varDec.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
const tags: [string, string, string] = [
@ -773,12 +794,17 @@ export class SceneEntities {
'VariableDeclaration'
)
if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init
const sketchInit = isNodeType<VariableDeclaration>(
_node.node,
'VariableDeclaration'
)
? _node.node.declarations[0]?.init
: null
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
if (sketchInit.type === 'PipeExpression') {
if (sketchInit?.type === 'PipeExpression') {
updateRectangleSketch(sketchInit, x, y, tags[0])
}
@ -821,9 +847,14 @@ export class SceneEntities {
'VariableDeclaration'
)
if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init
const sketchInit = isNodeType<VariableDeclaration>(
_node.node,
'VariableDeclaration'
)
? _node.node.declarations[0]?.init
: null
if (sketchInit.type === 'PipeExpression') {
if (sketchInit?.type === 'PipeExpression') {
updateRectangleSketch(sketchInit, x, y, tags[0])
let _recastAst = parse(recast(_ast))
@ -1059,7 +1090,7 @@ export class SceneEntities {
if (trap(_node)) return
const node = _node.node
if (node.type !== 'CallExpression') return
if (isArray(node) || node.type !== 'CallExpression') return
let modded:
| {
@ -1500,6 +1531,146 @@ export class SceneEntities {
this._tearDownSketch(0, resolve, reject, { removeAxis })
})
}
setupDefaultPlaneHover() {
sceneInfra.setCallbacks({
onMouseEnter: ({ selected }) => {
if (!(selected instanceof Mesh && selected.parent)) return
if (selected.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type, 0.5, 1)
},
onMouseLeave: ({ selected }) => {
if (!(selected instanceof Mesh && selected.parent)) return
if (selected.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = selected.userData.type
selected.material.color = defaultPlaneColor(type)
},
onClick: async (args) => {
const { entity_id } = await sendSelectEventToEngine(
args?.mouseEvent,
document.getElementById('video-stream') as HTMLVideoElement,
sceneInfra._streamDimensions
)
let _entity_id = entity_id
if (!_entity_id) return
if (
engineCommandManager.defaultPlanes?.xy === _entity_id ||
engineCommandManager.defaultPlanes?.xz === _entity_id ||
engineCommandManager.defaultPlanes?.yz === _entity_id ||
engineCommandManager.defaultPlanes?.negXy === _entity_id ||
engineCommandManager.defaultPlanes?.negXz === _entity_id ||
engineCommandManager.defaultPlanes?.negYz === _entity_id
) {
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
[engineCommandManager.defaultPlanes.xy]: 'XY',
[engineCommandManager.defaultPlanes.xz]: 'XZ',
[engineCommandManager.defaultPlanes.yz]: 'YZ',
[engineCommandManager.defaultPlanes.negXy]: '-XY',
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
}
// TODO can we get this information from rust land when it creates the default planes?
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
let zAxis: [number, number, number] = [0, 0, 1]
let yAxis: [number, number, number] = [0, 1, 0]
// get unit vector from camera position to target
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)
if (engineCommandManager.defaultPlanes?.xy === _entity_id) {
zAxis = [0, 0, 1]
yAxis = [0, 1, 0]
if (camVector.z < 0) {
zAxis = [0, 0, -1]
_entity_id = engineCommandManager.defaultPlanes?.negXy || ''
}
} else if (engineCommandManager.defaultPlanes?.yz === _entity_id) {
zAxis = [1, 0, 0]
yAxis = [0, 0, 1]
if (camVector.x < 0) {
zAxis = [-1, 0, 0]
_entity_id = engineCommandManager.defaultPlanes?.negYz || ''
}
} else if (engineCommandManager.defaultPlanes?.xz === _entity_id) {
zAxis = [0, 1, 0]
yAxis = [0, 0, 1]
_entity_id = engineCommandManager.defaultPlanes?.negXz || ''
if (camVector.y < 0) {
zAxis = [0, -1, 0]
_entity_id = engineCommandManager.defaultPlanes?.xz || ''
}
}
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'defaultPlane',
planeId: _entity_id,
plane: defaultPlaneStrMap[_entity_id],
zAxis,
yAxis,
},
})
return
}
const artifact = this.engineCommandManager.artifactMap[_entity_id]
// If we clicked on an extrude wall, we climb up the parent Id
// to get the sketch profile's face ID. If we clicked on an endcap,
// we already have it.
const pathId =
artifact?.type === 'extrudeWall' || artifact?.type === 'extrudeCap'
? artifact.pathId
: ''
// tsc cannot infer that target can have extrusions
// from the commandType (why?) so we need to cast it
const path = this.engineCommandManager.artifactMap?.[pathId || '']
const extrusionId =
path?.type === 'startPath' ? path.extrusionIds[0] : ''
// TODO: We get the first extrusion command ID,
// which is fine while backend systems only support one extrusion.
// but we need to more robustly handle resolving to the correct extrusion
// if there are multiple.
const extrusions = this.engineCommandManager.artifactMap?.[extrusionId]
if (artifact?.type !== 'extrudeCap' && artifact?.type !== 'extrudeWall')
return
const faceInfo = await getFaceDetails(_entity_id)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis) return
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
)
const extrudePathToNode = extrusions?.range
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
: []
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap: artifact.type === 'extrudeCap' ? artifact.cap : 'none',
faceId: _entity_id,
},
})
return
},
})
}
mouseEnterLeaveCallbacks() {
return {
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
@ -1524,7 +1695,9 @@ export class SceneEntities {
)
if (trap(_node, { suppress: true })) return
const node = _node.node
editorManager.setHighlightRange([node.start, node.end])
editorManager.setHighlightRange(
!isArray(node) ? [node.start, node.end] : [0, 0]
)
const yellow = 0xffff00
colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -1654,7 +1827,14 @@ function prepareTruncatedMemoryAndAst(
'VariableDeclaration'
)
if (err(_node)) return _node
const variableDeclarationName = _node.node?.declarations?.[0]?.id?.name || ''
if (isArray(_node.node))
return new Error('Expected node to be an object, but found Array')
const variableDeclarationName = isNodeType<VariableDeclaration>(
_node.node,
'VariableDeclaration'
)
? _node.node.declarations[0]?.id?.name || ''
: ''
const lastSeg = (
programMemory.get(variableDeclarationName) as SketchGroup
).value.slice(-1)[0]
@ -1777,7 +1957,12 @@ export function sketchGroupFromPathToNode({
)
if (err(_varDec)) return _varDec
const varDec = _varDec.node
const result = programMemory.get(varDec?.id?.name || '')
if (isArray(varDec))
return new Error('Expected node to be an object, but found Array')
const varName = isNodeType<VariableDeclarator>(varDec, 'VariableDeclarator')
? varDec.id.name
: ''
const result = programMemory.get(varName)
if (result?.type === 'ExtrudeGroup') {
return result.sketchGroup
}

View File

@ -11,8 +11,10 @@ import {
Raycaster,
Vector2,
Group,
PlaneGeometry,
MeshBasicMaterial,
Mesh,
DoubleSide,
Intersection,
Object3D,
Object3DEventMap,
@ -46,6 +48,7 @@ export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
export const DEBUG_SHOW_BOTH_SCENES: false = false
export const RAYCASTABLE_PLANE = 'raycastable-plane'
export const DEFAULT_PLANES = 'default-planes'
export const X_AXIS = 'xAxis'
export const Y_AXIS = 'yAxis'
@ -322,9 +325,16 @@ export class SceneInfra {
this.camControls.camera,
this.camControls.target
)
const planesGroup = this.scene.getObjectByName(DEFAULT_PLANES)
const axisGroup = this.scene
.getObjectByName(AXIS_GROUP)
?.getObjectByName('gridHelper')
planesGroup &&
planesGroup.scale.set(
scale / this._baseUnitMultiplier,
scale / this._baseUnitMultiplier,
scale / this._baseUnitMultiplier
)
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
}
@ -622,6 +632,59 @@ export class SceneInfra {
this.onClickCallback({ mouseEvent, intersects })
}
}
showDefaultPlanes() {
const addPlane = (
rotation: { x: number; y: number; z: number }, //
type: DefaultPlane
): Mesh => {
const planeGeometry = new PlaneGeometry(100, 100)
const planeMaterial = new MeshBasicMaterial({
color: defaultPlaneColor(type),
transparent: true,
opacity: 0.0,
side: DoubleSide,
depthTest: false, // needed to avoid transparency issues
})
const plane = new Mesh(planeGeometry, planeMaterial)
plane.rotation.x = rotation.x
plane.rotation.y = rotation.y
plane.rotation.z = rotation.z
plane.userData.type = type
plane.name = type
return plane
}
const planes = [
addPlane({ x: 0, y: Math.PI / 2, z: 0 }, YZ_PLANE),
addPlane({ x: 0, y: 0, z: 0 }, XY_PLANE),
addPlane({ x: -Math.PI / 2, y: 0, z: 0 }, XZ_PLANE),
]
const planesGroup = new Group()
planesGroup.userData.type = DEFAULT_PLANES
planesGroup.name = DEFAULT_PLANES
planesGroup.add(...planes)
planesGroup.traverse((child) => {
if (child instanceof Mesh) {
child.layers.enable(SKETCH_LAYER)
}
})
planesGroup.layers.enable(SKETCH_LAYER)
const sceneScale = getSceneScale(
this.camControls.camera,
this.camControls.target
)
planesGroup.scale.set(
sceneScale / this._baseUnitMultiplier,
sceneScale / this._baseUnitMultiplier,
sceneScale / this._baseUnitMultiplier
)
this.scene.add(planesGroup)
}
removeDefaultPlanes() {
const planesGroup = this.scene.children.find(
({ userData }) => userData.type === DEFAULT_PLANES
)
if (planesGroup) this.scene.remove(planesGroup)
}
updateOtherSelectionColors = (otherSelections: Axis[]) => {
const axisGroup = this.scene.children.find(
({ userData }) => userData?.type === AXIS_GROUP
@ -679,3 +742,28 @@ function baseUnitTomm(baseUnit: BaseUnit) {
return 914.4
}
}
export type DefaultPlane =
| 'xy-default-plane'
| 'xz-default-plane'
| 'yz-default-plane'
export const XY_PLANE: DefaultPlane = 'xy-default-plane'
export const XZ_PLANE: DefaultPlane = 'xz-default-plane'
export const YZ_PLANE: DefaultPlane = 'yz-default-plane'
export function defaultPlaneColor(
plane: DefaultPlane,
lowCh = 0.1,
highCh = 0.7
): Color {
switch (plane) {
case XY_PLANE:
return new Color(highCh, lowCh, lowCh)
case XZ_PLANE:
return new Color(lowCh, lowCh, highCh)
case YZ_PLANE:
return new Color(lowCh, highCh, lowCh)
}
return new Color(lowCh, lowCh, lowCh)
}

View File

@ -5,8 +5,6 @@ import { CustomIcon } from './CustomIcon'
import { useLocation, useNavigate } from 'react-router-dom'
import { createAndOpenNewProject } from 'lib/tauriFS'
import { paths } from 'lib/paths'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider'
const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -14,18 +12,16 @@ const HelpMenuDivider = () => (
export function HelpMenu(props: React.PropsWithChildren) {
const location = useLocation()
const { onProjectOpen } = useLspContext()
const filePath = useAbsoluteFilePath()
const isInProject = location.pathname.includes(paths.FILE)
const navigate = useNavigate()
const { settings } = useSettingsAuthContext()
return (
<Popover className="relative">
<Popover.Button className="grid p-0 m-0 border-none rounded-full place-content-center">
<Popover.Button className="border-none p-0 m-0 rounded-full grid place-content-center">
<CustomIcon
name="questionMark"
className="rounded-full w-7 h-7 bg-chalkboard-110 dark:bg-chalkboard-80 text-chalkboard-10"
className="w-7 h-7 rounded-full bg-chalkboard-110 dark:bg-chalkboard-80 text-chalkboard-10"
/>
<span className="sr-only">Help and resources</span>
<Tooltip position="top-right" wrapperClassName="ui-open:hidden">
@ -34,7 +30,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
</Popover.Button>
<Popover.Panel
as="ul"
className="absolute right-0 left-auto flex flex-col w-64 gap-1 p-0 py-2 m-0 mb-1 text-sm border border-solid rounded shadow-lg bottom-full align-stretch text-chalkboard-10 dark:text-inherit bg-chalkboard-110 dark:bg-chalkboard-100 border-chalkboard-110 dark:border-chalkboard-80"
className="absolute right-0 left-auto bottom-full mb-1 w-64 py-2 flex flex-col gap-1 align-stretch text-chalkboard-10 dark:text-inherit bg-chalkboard-110 dark:bg-chalkboard-100 rounded shadow-lg border border-solid border-chalkboard-110 dark:border-chalkboard-80 text-sm m-0 p-0"
>
<HelpMenuItem
as="a"
@ -88,12 +84,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
</HelpMenuItem>
<HelpMenuItem
as="button"
onClick={() => {
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS
: paths.HOME + paths.SETTINGS
navigate(targetPath + '?tab=keybindings')
}}
onClick={() => navigate('settings?tab=keybindings')}
>
Keyboard shortcuts
</HelpMenuItem>
@ -108,9 +99,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
},
})
if (isInProject) {
navigate(filePath + paths.ONBOARDING.INDEX)
navigate('onboarding')
} else {
createAndOpenNewProject({ onProjectOpen, navigate })
createAndOpenNewProject(navigate)
}
}}
>
@ -137,7 +128,7 @@ function HelpMenuItem({
}: HelpMenuItemProps) {
const baseClassName = 'block px-2 py-1 hover:bg-chalkboard-80'
return (
<li className="p-0 m-0">
<li className="m-0 p-0">
{as === 'a' ? (
<a
{...(props as React.ComponentProps<'a'>)}

View File

@ -1,5 +1,5 @@
import { useMachine } from '@xstate/react'
import React, { createContext, useEffect, useMemo, useRef } from 'react'
import React, { createContext, useEffect, useRef } from 'react'
import {
AnyStateMachine,
ContextFrom,
@ -8,12 +8,7 @@ import {
StateFrom,
assign,
} from 'xstate'
import {
SetSelections,
getPersistedContext,
modelingMachine,
modelingMachineDefaultContext,
} from 'machines/modelingMachine'
import { SetSelections, modelingMachine } from 'machines/modelingMachine'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import {
@ -104,7 +99,6 @@ export const ModelingMachineProvider = ({
} = useSettingsAuthContext()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), [])
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
@ -127,13 +121,6 @@ export const ModelingMachineProvider = ({
const [modelingState, modelingSend, modelingActor] = useMachine(
modelingMachine,
{
context: {
...modelingMachineDefaultContext,
store: {
...modelingMachineDefaultContext.store,
...persistedContext,
},
},
actions: {
'disable copilot': () => {
editorManager.setCopilotEnabled(false)

View File

@ -27,3 +27,27 @@ export const LogsPane = () => {
</div>
)
}
export const KclErrorsPane = () => {
const theme = useResolvedTheme()
const { errors } = useKclContext()
return (
<div className="overflow-hidden">
<div className="absolute inset-0 p-2 flex flex-col overflow-auto">
<ReactJsonTypeHack
src={errors}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
)
}

View File

@ -3,6 +3,7 @@ import {
faBugSlash,
faCode,
faCodeCommit,
faExclamationCircle,
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu'
@ -10,7 +11,7 @@ import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
import { ReactNode } from 'react'
import { MemoryPane, MemoryPaneMenu } from './MemoryPane'
import { LogsPane } from './LoggingPanes'
import { KclErrorsPane, LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
import { useKclContext } from 'lang/KclProvider'
@ -20,6 +21,7 @@ export type SidebarType =
| 'debug'
| 'export'
| 'files'
| 'kclErrors'
| 'logs'
| 'lspMessages'
| 'variables'
@ -51,7 +53,6 @@ export const sidebarPanes: SidebarPane[] = [
Content: KclEditorPane,
keybinding: 'Shift + C',
Menu: KclEditorMenu,
showBadge: ({ kclContext }) => kclContext.errors.length,
},
{
id: 'files',
@ -77,6 +78,14 @@ export const sidebarPanes: SidebarPane[] = [
Content: LogsPane,
keybinding: 'Shift + L',
},
{
id: 'kclErrors',
title: 'KCL Errors',
icon: faExclamationCircle,
Content: KclErrorsPane,
keybinding: 'Shift + E',
showBadge: ({ kclContext }) => kclContext.errors.length,
},
{
id: 'debug',
title: 'Debug',

View File

@ -19,8 +19,7 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
import { paths } from 'lib/paths'
import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { sep } from '@tauri-apps/api/path'
import { ForwardedRef, forwardRef, useEffect } from 'react'
import { useLspContext } from 'components/LspProvider'
import { ForwardedRef, forwardRef } from 'react'
interface AllSettingsFieldsProps {
searchParamTab: SettingsLevel
@ -34,10 +33,9 @@ export const AllSettingsFields = forwardRef(
) => {
const location = useLocation()
const navigate = useNavigate()
const { onProjectOpen } = useLspContext()
const dotDotSlash = useDotDotSlash()
const {
settings: { send, context, state },
settings: { send, context },
} = useSettingsAuthContext()
const projectPath =
@ -50,36 +48,18 @@ export const AllSettingsFields = forwardRef(
)
: undefined
async function restartOnboarding() {
function restartOnboarding() {
send({
type: `set.app.onboardingStatus`,
data: { level: 'user', value: '' },
})
}
/**
* A "listener" for the XState to return to "idle" state
* when the user resets the onboarding, using the callback above
*/
useEffect(() => {
async function navigateToOnboardingStart() {
if (
state.context.app.onboardingStatus.user === '' &&
state.matches('idle')
) {
if (isFileSettings) {
// If we're in a project, first navigate to the onboarding start here
// so we can trigger the warning screen if necessary
navigate(dotDotSlash(1) + paths.ONBOARDING.INDEX)
} else {
// If we're in the global settings, create a new project and navigate
// to the onboarding start in that project
await createAndOpenNewProject({ onProjectOpen, navigate })
}
}
if (isFileSettings) {
navigate(dotDotSlash(1) + paths.ONBOARDING.INDEX)
} else {
createAndOpenNewProject(navigate)
}
navigateToOnboardingStart()
}, [isFileSettings, navigate, state])
}
return (
<div className="relative overflow-y-auto">

View File

@ -225,7 +225,7 @@ export const Stream = () => {
},
})
if (state.matches('Sketch')) return
if (state.matches('idle.showPlanes')) return
if (state.matches('Sketch no face')) return
if (!context.store?.didDragInStream && btnName(e).left) {
sendSelectEventToEngine(

View File

@ -1,9 +1,12 @@
import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
import { CallExpression, Program, VariableDeclarator } from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
getLastNodeFromPath,
DynamicNode,
isNodeType,
expectNodeOnPath,
} from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import {
@ -14,6 +17,7 @@ import {
} from '../../lang/std/sketchcombos'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { isArray } from 'lib/utils'
export function equalAngleInfo({
selectionRanges,
@ -29,26 +33,33 @@ export function equalAngleInfo({
getNodePathFromSourceRange(kclManager.ast, range)
)
const _nodes = paths.map((pathToNode) => {
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
if (err(tmp)) return tmp
if (isArray(tmp.node)) {
return new Error('Expected value node, but found array')
}
return tmp.node
})
const _err1 = _nodes.find(err)
if (err(_err1)) return _err1
const nodes = _nodes as Value[]
const nodes: DynamicNode[] = []
for (const node of _nodes) {
if (err(node)) return node
nodes.push(node)
}
const _varDecs = paths.map((pathToNode) => {
const tmp = getNodeFromPath<VariableDeclarator>(
const tmp = expectNodeOnPath<VariableDeclarator>(
kclManager.ast,
pathToNode,
'VariableDeclarator'
)
if (err(tmp)) return tmp
return tmp.node
return tmp
})
const _err2 = _varDecs.find(err)
if (err(_err2)) return _err2
const varDecs = _varDecs as VariableDeclarator[]
const varDecs: VariableDeclarator[] = []
for (const varDec of _varDecs) {
if (err(varDec)) return varDec
varDecs.push(varDec)
}
const primaryLine = varDecs[0]
const secondaryVarDecs = varDecs.slice(1)
@ -57,7 +68,7 @@ export function equalAngleInfo({
)
const isAllTooltips = nodes.every(
(node) =>
node?.type === 'CallExpression' &&
isNodeType<CallExpression>(node, 'CallExpression') &&
toolTips.includes(node.callee.name as any)
)

View File

@ -1,9 +1,12 @@
import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
import { CallExpression, Program, VariableDeclarator } from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
getLastNodeFromPath,
DynamicNode,
isNodeType,
expectNodeOnPath,
} from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import {
@ -14,6 +17,7 @@ import {
} from '../../lang/std/sketchcombos'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { isArray } from 'lib/utils'
export function setEqualLengthInfo({
selectionRanges,
@ -29,26 +33,33 @@ export function setEqualLengthInfo({
getNodePathFromSourceRange(kclManager.ast, range)
)
const _nodes = paths.map((pathToNode) => {
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
if (err(tmp)) return tmp
if (isArray(tmp.node)) {
return new Error('Expected value node, but found array')
}
return tmp.node
})
const _err1 = _nodes.find(err)
if (err(_err1)) return _err1
const nodes = _nodes as Value[]
const nodes: DynamicNode[] = []
for (const node of _nodes) {
if (err(node)) return node
nodes.push(node)
}
const _varDecs = paths.map((pathToNode) => {
const tmp = getNodeFromPath<VariableDeclarator>(
const varDec = expectNodeOnPath<VariableDeclarator>(
kclManager.ast,
pathToNode,
'VariableDeclarator'
)
if (err(tmp)) return tmp
return tmp.node
if (err(varDec)) return varDec
return varDec
})
const _err2 = _varDecs.find(err)
if (err(_err2)) return _err2
const varDecs = _varDecs as VariableDeclarator[]
const varDecs: VariableDeclarator[] = []
for (const varDec of _varDecs) {
if (err(varDec)) return varDec
varDecs.push(varDec)
}
const primaryLine = varDecs[0]
const secondaryVarDecs = varDecs.slice(1)
@ -57,7 +68,7 @@ export function setEqualLengthInfo({
)
const isAllTooltips = nodes.every(
(node) =>
node?.type === 'CallExpression' &&
isNodeType<CallExpression>(node, 'CallExpression') &&
toolTips.includes(node.callee.name as any)
)

View File

@ -1,9 +1,11 @@
import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { Program, ProgramMemory, Value } from '../../lang/wasm'
import { CallExpression, Program, ProgramMemory } from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
getLastNodeFromPath,
DynamicNode,
isNodeType,
} from '../../lang/queryAst'
import {
PathToNodeMap,
@ -13,6 +15,7 @@ import {
} from '../../lang/std/sketchcombos'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { isArray } from 'lib/utils'
export function horzVertInfo(
selectionRanges: Selections,
@ -27,17 +30,22 @@ export function horzVertInfo(
getNodePathFromSourceRange(kclManager.ast, range)
)
const _nodes = paths.map((pathToNode) => {
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
if (err(tmp)) return tmp
if (isArray(tmp.node)) {
return new Error('Expected value node, but found array')
}
return tmp.node
})
const _err1 = _nodes.find(err)
if (err(_err1)) return _err1
const nodes = _nodes as Value[]
const nodes: DynamicNode[] = []
for (const node of _nodes) {
if (err(node)) return node
nodes.push(node)
}
const isAllTooltips = nodes.every(
(node) =>
node?.type === 'CallExpression' &&
isNodeType<CallExpression>(node, 'CallExpression') &&
toolTips.includes(node.callee.name as any)
)

View File

@ -1,10 +1,18 @@
import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
import {
BinaryPart,
CallExpression,
Program,
VariableDeclarator,
} from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
isLinesParallelAndConstrained,
getLastNodeFromPath,
expectNodeOnPath,
DynamicNode,
isNodeType,
} from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import {
@ -18,6 +26,7 @@ import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { isArray } from 'lib/utils'
const getModalInfo = createInfoModal(GetInfoModal)
@ -72,26 +81,33 @@ export function intersectInfo({
getNodePathFromSourceRange(kclManager.ast, range)
)
const _nodes = paths.map((pathToNode) => {
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
if (err(tmp)) return tmp
if (isArray(tmp.node)) {
return new Error('Expected value node, but found array')
}
return tmp.node
})
const _err1 = _nodes.find(err)
if (err(_err1)) return _err1
const nodes = _nodes as Value[]
const nodes: DynamicNode[] = []
for (const node of _nodes) {
if (err(node)) return node
nodes.push(node)
}
const _varDecs = paths.map((pathToNode) => {
const tmp = getNodeFromPath<VariableDeclarator>(
const varDec = expectNodeOnPath<VariableDeclarator>(
kclManager.ast,
pathToNode,
'VariableDeclarator'
)
if (err(tmp)) return tmp
return tmp.node
if (err(varDec)) return varDec
return varDec
})
const _err2 = _varDecs.find(err)
if (err(_err2)) return _err2
const varDecs = _varDecs as VariableDeclarator[]
const varDecs: VariableDeclarator[] = []
for (const varDec of _varDecs) {
if (err(varDec)) return varDec
varDecs.push(varDec)
}
const primaryLine = varDecs[0]
const secondaryVarDecs = varDecs.slice(1)
@ -100,7 +116,7 @@ export function intersectInfo({
)
const isAllTooltips = nodes.every(
(node) =>
node?.type === 'CallExpression' &&
isNodeType<CallExpression>(node, 'CallExpression') &&
[
...toolTips,
'startSketchAt', // TODO probably a better place for this to live

View File

@ -1,9 +1,11 @@
import { toolTips } from 'lang/langHelpers'
import { Selection, Selections } from 'lib/selections'
import { PathToNode, Program, Value } from '../../lang/wasm'
import { CallExpression, PathToNode, Program } from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
getLastNodeFromPath,
DynamicNode,
isNodeType,
} from '../../lang/queryAst'
import {
PathToNodeMap,
@ -13,6 +15,7 @@ import {
} from '../../lang/std/sketchcombos'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { isArray } from 'lib/utils'
export function removeConstrainingValuesInfo({
selectionRanges,
@ -33,13 +36,18 @@ export function removeConstrainingValuesInfo({
getNodePathFromSourceRange(kclManager.ast, range)
)
const _nodes = paths.map((pathToNode) => {
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
if (err(tmp)) return tmp
if (isArray(tmp.node)) {
return new Error('Expected value node, but found array')
}
return tmp.node
})
const _err1 = _nodes.find(err)
if (err(_err1)) return _err1
const nodes = _nodes as Value[]
const nodes: DynamicNode[] = []
for (const node of _nodes) {
if (err(node)) return node
nodes.push(node)
}
const updatedSelectionRanges = pathToNodes
? {
@ -54,7 +62,7 @@ export function removeConstrainingValuesInfo({
: selectionRanges
const isAllTooltips = nodes.every(
(node) =>
node?.type === 'CallExpression' &&
isNodeType<CallExpression>(node, 'CallExpression') &&
toolTips.includes(node.callee.name as any)
)

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { BinaryPart, Program, Value } from '../../lang/wasm'
import { BinaryPart, CallExpression, Program, Value } from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -49,22 +49,22 @@ export function absDistanceInfo({
getNodePathFromSourceRange(kclManager.ast, range)
)
const _nodes = paths.map((pathToNode) => {
const tmp = getNodeFromPath<Value>(
const tmp = getNodeFromPath<CallExpression>(
kclManager.ast,
pathToNode,
'CallExpression'
)
if (err(tmp)) return tmp
return tmp.node
return tmp.stopAtNode
})
const _err1 = _nodes.find(err)
if (err(_err1)) return _err1
const nodes = _nodes as Value[]
const nodes: (CallExpression | null)[] = []
for (const node of _nodes) {
if (err(node)) return node
nodes.push(node)
}
const isAllTooltips = nodes.every(
(node) =>
node?.type === 'CallExpression' &&
toolTips.includes(node.callee.name as any)
(node) => node && toolTips.includes(node.callee.name as any)
)
const transforms = getTransformInfos(selectionRanges, kclManager.ast, disType)

View File

@ -1,9 +1,17 @@
import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
import {
BinaryPart,
CallExpression,
Program,
VariableDeclarator,
} from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
getLastNodeFromPath,
DynamicNode,
expectNodeOnPath,
isNodeType,
} from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import {
@ -17,6 +25,7 @@ import { createVariableDeclaration } from '../../lang/modifyAst'
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { isArray } from 'lib/utils'
const getModalInfo = createInfoModal(GetInfoModal)
@ -35,26 +44,33 @@ export function angleBetweenInfo({
)
const _nodes = paths.map((pathToNode) => {
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
if (err(tmp)) return tmp
if (isArray(tmp.node)) {
return new Error('Expected value node, but found array')
}
return tmp.node
})
const _err1 = _nodes.find(err)
if (err(_err1)) return _err1
const nodes = _nodes as Value[]
const nodes: DynamicNode[] = []
for (const node of _nodes) {
if (err(node)) return node
nodes.push(node)
}
const _varDecs = paths.map((pathToNode) => {
const tmp = getNodeFromPath<VariableDeclarator>(
const varDec = expectNodeOnPath<VariableDeclarator>(
kclManager.ast,
pathToNode,
'VariableDeclarator'
)
if (err(tmp)) return tmp
return tmp.node
if (err(varDec)) return varDec
return varDec
})
const _err2 = _varDecs.find(err)
if (err(_err2)) return _err2
const varDecs = _varDecs as VariableDeclarator[]
const varDecs: VariableDeclarator[] = []
for (const varDec of _varDecs) {
if (err(varDec)) return varDec
varDecs.push(varDec)
}
const primaryLine = varDecs[0]
const secondaryVarDecs = varDecs.slice(1)
@ -63,7 +79,7 @@ export function angleBetweenInfo({
)
const isAllTooltips = nodes.every(
(node) =>
node?.type === 'CallExpression' &&
isNodeType<CallExpression>(node, 'CallExpression') &&
toolTips.includes(node.callee.name as any)
)

View File

@ -1,8 +1,15 @@
import { toolTips } from 'lang/langHelpers'
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
import {
BinaryPart,
CallExpression,
Program,
VariableDeclarator,
} from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
getLastNodeFromPath,
expectNodeOnPath,
isNodeType,
} from '../../lang/queryAst'
import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints'
import {
@ -17,6 +24,7 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { kclManager } from 'lib/singletons'
import { Selections } from 'lib/selections'
import { cleanErrs, err } from 'lib/trap'
import { isArray } from 'lib/utils'
const getModalInfo = createInfoModal(GetInfoModal)
@ -36,27 +44,31 @@ export function horzVertDistanceInfo({
getNodePathFromSourceRange(kclManager.ast, range)
)
const _nodes = paths.map((pathToNode) => {
const tmp = getNodeFromPath<Value>(kclManager.ast, pathToNode)
const tmp = getLastNodeFromPath(kclManager.ast, pathToNode)
if (err(tmp)) return tmp
if (isArray(tmp.node)) {
return new Error('Expected value node, but found array')
}
return tmp.node
})
const [hasErr, , nodesWErrs] = cleanErrs(_nodes)
const [hasErr, nodes, nodesWErrs] = cleanErrs(_nodes)
if (hasErr) return nodesWErrs[0]
const nodes = _nodes as Value[]
const _varDecs = paths.map((pathToNode) => {
const tmp = getNodeFromPath<VariableDeclarator>(
const varDec = expectNodeOnPath<VariableDeclarator>(
kclManager.ast,
pathToNode,
'VariableDeclarator'
)
if (err(tmp)) return tmp
return tmp.node
if (err(varDec)) return varDec
return varDec
})
const _err2 = _varDecs.find(err)
if (err(_err2)) return _err2
const varDecs = _varDecs as VariableDeclarator[]
const varDecs: VariableDeclarator[] = []
for (const varDec of _varDecs) {
if (err(varDec)) return varDec
varDecs.push(varDec)
}
const primaryLine = varDecs[0]
const secondaryVarDecs = varDecs.slice(1)
@ -65,7 +77,7 @@ export function horzVertDistanceInfo({
)
const isAllTooltips = nodes.every(
(node) =>
node?.type === 'CallExpression' &&
isNodeType<CallExpression>(node, 'CallExpression') &&
[
...toolTips,
'startSketchAt', // TODO probably a better place for this to live

View File

@ -1,6 +1,6 @@
import { toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { BinaryPart, Program, Value } from '../../lang/wasm'
import { BinaryPart, CallExpression, Program } from '../../lang/wasm'
import {
getNodePathFromSourceRange,
getNodeFromPath,
@ -43,18 +43,24 @@ export function angleLengthInfo({
getNodePathFromSourceRange(kclManager.ast, range)
)
const nodes = paths.map((pathToNode) =>
getNodeFromPath<Value>(kclManager.ast, pathToNode, 'CallExpression')
)
const _err1 = nodes.find(err)
if (err(_err1)) return _err1
const isAllTooltips = nodes.every((meta) => {
if (err(meta)) return false
return (
meta.node?.type === 'CallExpression' &&
toolTips.includes(meta.node.callee.name as any)
const _nodes = paths.map((pathToNode) => {
const tmp = getNodeFromPath<CallExpression>(
kclManager.ast,
pathToNode,
'CallExpression'
)
if (err(tmp)) return tmp
return tmp.stopAtNode
})
const nodes: (CallExpression | null)[] = []
for (const node of _nodes) {
if (err(node)) return node
nodes.push(node)
}
const isAllTooltips = nodes.every((node) => {
if (err(node)) return false
return node && toolTips.includes(node.callee.name as any)
})
const transforms = getTransformInfos(

View File

@ -57,8 +57,7 @@
transition-delay: var(--_delay);
}
:is(:focus-visible) > .tooltipWrapper.withFocus,
:focus-within > .tooltipWrapper.withFocus {
:is(:focus-visible) > .tooltipWrapper.withFocus {
visibility: visible;
opacity: 1;
}

View File

@ -29,7 +29,7 @@ export default function Tooltip({
return (
<div
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
{...{ inert: inert ? '' : undefined }}
inert={inert}
role="tooltip"
className={`p-3 ${
position !== 'left' && position !== 'right' ? 'px-0' : ''

View File

@ -1,17 +1,10 @@
import { useEffect } from 'react'
import {
editorManager,
engineCommandManager,
kclManager,
sceneInfra,
} from 'lib/singletons'
import { editorManager, engineCommandManager } from 'lib/singletons'
import { useModelingContext } from './useModelingContext'
import { getEventForSelectWithPoint } from 'lib/selections'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { getNodePathFromSourceRange } from 'lang/queryAst'
export function useEngineConnectionSubscriptions() {
const { send, context, state } = useModelingContext()
const { send, context } = useModelingContext()
useEffect(() => {
if (!engineCommandManager) return
@ -47,135 +40,4 @@ export function useEngineConnectionSubscriptions() {
unSubClick()
}
}, [engineCommandManager, context?.sketchEnginePathId])
useEffect(() => {
const unSub = engineCommandManager.subscribeTo({
event: 'select_with_point',
callback: state.matches('Sketch no face')
? async ({ data }) => {
let planeId = data.entity_id
if (!planeId) return
if (
engineCommandManager.defaultPlanes?.xy === planeId ||
engineCommandManager.defaultPlanes?.xz === planeId ||
engineCommandManager.defaultPlanes?.yz === planeId ||
engineCommandManager.defaultPlanes?.negXy === planeId ||
engineCommandManager.defaultPlanes?.negXz === planeId ||
engineCommandManager.defaultPlanes?.negYz === planeId
) {
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
[engineCommandManager.defaultPlanes.xy]: 'XY',
[engineCommandManager.defaultPlanes.xz]: 'XZ',
[engineCommandManager.defaultPlanes.yz]: 'YZ',
[engineCommandManager.defaultPlanes.negXy]: '-XY',
[engineCommandManager.defaultPlanes.negXz]: '-XZ',
[engineCommandManager.defaultPlanes.negYz]: '-YZ',
}
// TODO can we get this information from rust land when it creates the default planes?
// maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
let zAxis: [number, number, number] = [0, 0, 1]
let yAxis: [number, number, number] = [0, 1, 0]
// get unit vector from camera position to target
const camVector = sceneInfra.camControls.camera.position
.clone()
.sub(sceneInfra.camControls.target)
if (engineCommandManager.defaultPlanes?.xy === planeId) {
zAxis = [0, 0, 1]
yAxis = [0, 1, 0]
if (camVector.z < 0) {
zAxis = [0, 0, -1]
planeId = engineCommandManager.defaultPlanes?.negXy || ''
}
} else if (engineCommandManager.defaultPlanes?.yz === planeId) {
zAxis = [1, 0, 0]
yAxis = [0, 0, 1]
if (camVector.x < 0) {
zAxis = [-1, 0, 0]
planeId = engineCommandManager.defaultPlanes?.negYz || ''
}
} else if (engineCommandManager.defaultPlanes?.xz === planeId) {
zAxis = [0, 1, 0]
yAxis = [0, 0, 1]
planeId = engineCommandManager.defaultPlanes?.negXz || ''
if (camVector.y < 0) {
zAxis = [0, -1, 0]
planeId = engineCommandManager.defaultPlanes?.xz || ''
}
}
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'defaultPlane',
planeId: planeId,
plane: defaultPlaneStrMap[planeId],
zAxis,
yAxis,
},
})
return
}
const artifact = engineCommandManager.artifactMap[planeId]
console.log('artifact', artifact)
// If we clicked on an extrude wall, we climb up the parent Id
// to get the sketch profile's face ID. If we clicked on an endcap,
// we already have it.
const pathId =
artifact?.type === 'extrudeWall' ||
artifact?.type === 'extrudeCap'
? artifact.pathId
: ''
const path = engineCommandManager.artifactMap?.[pathId || '']
const extrusionId =
path?.type === 'startPath' ? path.extrusionIds[0] : ''
// TODO: We get the first extrusion command ID,
// which is fine while backend systems only support one extrusion.
// but we need to more robustly handle resolving to the correct extrusion
// if there are multiple.
const extrusions = engineCommandManager.artifactMap?.[extrusionId]
if (
artifact?.type !== 'extrudeCap' &&
artifact?.type !== 'extrudeWall'
)
return
const faceInfo = await getFaceDetails(planeId)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return
const { z_axis, y_axis, origin } = faceInfo
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
artifact.range
)
const extrudePathToNode = extrusions?.range
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
: []
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
type: 'extrudeFace',
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
position: [origin.x, origin.y, origin.z].map(
(num) => num / sceneInfra._baseUnitMultiplier
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap: artifact.type === 'extrudeCap' ? artifact.cap : 'none',
faceId: planeId,
},
})
return
}
: () => {},
})
return unSub
}, [state])
}

View File

@ -1,7 +1,7 @@
import { executeAst, lintAst } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils'
import { isArray, uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection'
import { err } from 'lib/trap'
@ -15,12 +15,17 @@ import {
recast,
SourceRange,
} from 'lang/wasm'
import { getNodeFromPath } from './queryAst'
import {
expectNodeOnPath,
getLastNodeFromPath,
getNodeFromPath,
} from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
export class KclManager {
private _ast: Program = {
type: 'Program',
body: [],
start: 0,
end: 0,
@ -156,6 +161,7 @@ export class KclManager {
clearAst() {
this._ast = {
type: 'Program',
body: [],
start: 0,
end: 0,
@ -313,14 +319,14 @@ export class KclManager {
Object.entries(this.engineCommandManager.artifactMap).forEach(
([commandId, artifact]) => {
if (!artifact.pathToNode) return
const _node1 = getNodeFromPath<CallExpression>(
const _node = getNodeFromPath<CallExpression>(
this.ast,
artifact.pathToNode,
'CallExpression'
)
if (err(_node1)) return
const { node } = _node1
if (node.type !== 'CallExpression') return
if (err(_node)) return
const { node } = _node
if (isArray(node) || node.type !== 'CallExpression') return
const [oldStart, oldEnd] = artifact.range
if (oldStart === 0 && oldEnd === 0) return
if (oldStart === node.start && oldEnd === node.end) return
@ -392,12 +398,15 @@ export class KclManager {
let returnVal: Selections | undefined = undefined
if (optionalParams?.focusPath) {
const _node1 = getNodeFromPath<any>(
const _node1 = getLastNodeFromPath(
astWithUpdatedSource,
optionalParams?.focusPath
)
if (err(_node1)) return Promise.reject(_node1)
const { node } = _node1
if (isArray(node)) {
return Promise.reject(new Error('Expected node to not be an array'))
}
const { start, end } = node
if (!start || !end)

View File

@ -1,4 +1,5 @@
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
import { isArray } from 'lib/utils'
import { getNodePathFromSourceRange, getLastNodeFromPath } from './queryAst'
import { Identifier, parse, initPromise, Parameter } from './wasm'
import { err } from 'lib/trap'
@ -25,10 +26,13 @@ const sk3 = startSketchAt([0, 0])
const ast = parse(code)
if (err(ast)) throw ast
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const _node = getNodeFromPath<any>(ast, nodePath)
const _node = getLastNodeFromPath(ast, nodePath)
if (err(_node)) throw _node
const { node } = _node
if (isArray(node)) {
throw new Error('Expected call expression node, but found array')
}
expect([node.start, node.end]).toEqual(sourceRange)
expect(node.type).toBe('CallExpression')
})
@ -53,7 +57,7 @@ const b1 = cube([0,0], 10)`
const ast = parse(code)
if (err(ast)) throw ast
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const _node = getNodeFromPath<Parameter>(ast, nodePath)
const _node = getLastNodeFromPath(ast, nodePath)
if (err(_node)) throw _node
const node = _node.node
@ -66,8 +70,12 @@ const b1 = cube([0,0], 10)`
['params', 'FunctionExpression'],
[0, 'index'],
])
if (isArray(node)) {
throw new Error('Expected parameter node, but found array')
}
expect(node.type).toBe('Parameter')
expect(node.identifier.name).toBe('pos')
const param = node as any as Parameter
expect(param.identifier.name).toBe('pos')
})
it('gets path right for deep within function definition body', () => {
const code = `fn cube = (pos, scale) => {
@ -90,7 +98,7 @@ const b1 = cube([0,0], 10)`
const ast = parse(code)
if (err(ast)) throw ast
const nodePath = getNodePathFromSourceRange(ast, sourceRange)
const _node = getNodeFromPath<Identifier>(ast, nodePath)
const _node = getLastNodeFromPath(ast, nodePath)
if (err(_node)) throw _node
const node = _node.node
expect(nodePath).toEqual([
@ -112,7 +120,11 @@ const b1 = cube([0,0], 10)`
['elements', 'ArrayExpression'],
[0, 'index'],
])
if (isArray(node)) {
throw new Error('Expected identifier node, but found array')
}
expect(node.type).toBe('Identifier')
expect(node.name).toBe('scale')
const ident = node as any as Identifier
expect(ident.name).toBe('scale')
})
})

View File

@ -119,6 +119,7 @@ describe('Testing addSketchTo', () => {
it('should add a sketch to a program', () => {
const result = addSketchTo(
{
type: 'Program',
body: [],
start: 0,
end: 0,

View File

@ -27,6 +27,8 @@ import {
getNodePathFromSourceRange,
isNodeSafeToReplace,
traverse,
getLastNodeFromPath,
expectNodeOnPath,
} from './queryAst'
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
import {
@ -36,7 +38,7 @@ import {
transformAstSketchLines,
} from './std/sketchcombos'
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities'
import { isOverlap, roundOff } from 'lib/utils'
import { isArray, isOverlap, roundOff } from 'lib/utils'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import { ConstrainInfo } from './std/stdTypes'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
@ -79,16 +81,12 @@ export function addStartProfileAt(
pathToNode: PathToNode,
at: [number, number]
): { modifiedAst: Program; pathToNode: PathToNode } | Error {
const _node1 = getNodeFromPath<VariableDeclaration>(
const variableDeclaration = expectNodeOnPath<VariableDeclaration>(
node,
pathToNode,
'VariableDeclaration'
)
if (err(_node1)) return _node1
const variableDeclaration = _node1.node
if (variableDeclaration.type !== 'VariableDeclaration') {
return new Error('variableDeclaration.init.type !== PipeExpression')
}
if (err(variableDeclaration)) return variableDeclaration
const _node = { ...node }
const init = variableDeclaration.declarations[0].init
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
@ -263,7 +261,7 @@ export function extrudeSketch(
}
| Error {
const _node = { ...node }
const _node1 = getNodeFromPath(_node, pathToNode)
const _node1 = getLastNodeFromPath(_node, pathToNode)
if (err(_node1)) return _node1
const { node: sketchExpression } = _node1
@ -274,9 +272,9 @@ export function extrudeSketch(
'PipeExpression'
)
if (err(_node2)) return _node2
const { node: pipeExpression } = _node2
const { stopAtNode: pipeExpression } = _node2
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
const isInPipeExpression = !!pipeExpression
const _node3 = getNodeFromPath<VariableDeclarator>(
_node,
@ -284,7 +282,11 @@ export function extrudeSketch(
'VariableDeclarator'
)
if (err(_node3)) return _node3
const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3
const { stopAtNode: variableDeclarator, shallowPath: pathToDecleration } =
_node3
if (!variableDeclarator) {
return new Error('VariableDeclarator not found')
}
const extrudeCall = createCallExpressionStdLib('extrude', [
distance,
@ -356,34 +358,34 @@ export function sketchOnExtrudedFace(
node,
KCL_DEFAULT_CONSTANT_PREFIXES.SKETCH
)
const _node1 = getNodeFromPath<VariableDeclarator>(
const oldSketchNode = expectNodeOnPath<VariableDeclarator>(
_node,
sketchPathToNode,
'VariableDeclarator',
true
{
firstFound: true,
message: 'Old sketch node not found',
}
)
if (err(_node1)) return _node1
const { node: oldSketchNode } = _node1
if (err(oldSketchNode)) return oldSketchNode
const oldSketchName = oldSketchNode.id.name
const _node2 = getNodeFromPath<CallExpression>(
const expression = expectNodeOnPath<CallExpression>(
_node,
sketchPathToNode,
'CallExpression'
)
if (err(_node2)) return _node2
const { node: expression } = _node2
if (err(expression)) return expression
const _node3 = getNodeFromPath<VariableDeclarator>(
const extrudeVarDec = expectNodeOnPath<VariableDeclarator>(
_node,
extrudePathToNode,
'VariableDeclarator'
)
if (err(_node3)) return _node3
const { node: extrudeVarDec } = _node3
const extrudeName = extrudeVarDec.id?.name
if (err(extrudeVarDec)) return extrudeVarDec
const extrudeName = extrudeVarDec.id.name
let _tag = null
let _tag: Identifier | Literal | null = null
if (cap === 'none') {
const __tag = addTagForSketchOnFace(
{
@ -678,9 +680,12 @@ export function giveSketchFnCallTag(
}
| Error {
const path = getNodePathFromSourceRange(ast, range)
const _node1 = getNodeFromPath<CallExpression>(ast, path, 'CallExpression')
if (err(_node1)) return _node1
const { node: primaryCallExp } = _node1
const primaryCallExp = expectNodeOnPath<CallExpression>(
ast,
path,
'CallExpression'
)
if (err(primaryCallExp)) return primaryCallExp
// Tag is always 3rd expression now, using arg index feels brittle
// but we can come up with a better way to identify tag later.
@ -784,27 +789,35 @@ export function deleteSegmentFromPipeExpression(
): Program | Error {
let _modifiedAst = structuredClone(modifiedAst)
dependentRanges.forEach((range) => {
for (const range of dependentRanges) {
const path = getNodePathFromSourceRange(_modifiedAst, range)
const callExp = getNodeFromPath<CallExpression>(
const _callExp = getNodeFromPath<CallExpression>(
_modifiedAst,
path,
'CallExpression',
true
)
if (err(callExp)) return callExp
if (err(_callExp)) return _callExp
const callExp = _callExp.stopAtNode
if (!callExp) {
return new Error('Call Expression not found')
}
const constraintInfo = getConstraintInfo(callExp.node, code, path).find(
const constraintInfo = getConstraintInfo(callExp, code, path).find(
({ sourceRange }) => isOverlap(sourceRange, range)
)
if (!constraintInfo) return
if (!constraintInfo) {
return new Error('Constraint Info not found')
}
const input = makeRemoveSingleConstraintInput(
constraintInfo.argPosition,
callExp.shallowPath
_callExp.shallowPath
)
if (!input) return
if (!input) {
return new Error('Input not found')
}
const transform = removeSingleConstraintInfo(
{
...input,
@ -812,11 +825,13 @@ export function deleteSegmentFromPipeExpression(
_modifiedAst,
programMemory
)
if (!transform) return
if (!transform) {
return new Error('Transform not found')
}
_modifiedAst = transform.modifiedAst
})
}
const pipeExpression = getNodeFromPath<PipeExpression>(
const pipeExpression = expectNodeOnPath<PipeExpression>(
_modifiedAst,
pathToNode,
'PipeExpression'
@ -827,7 +842,7 @@ export function deleteSegmentFromPipeExpression(
([_, desc]) => desc === 'PipeExpression'
)
const segmentIndexInPipe = pathToNode[pipeInPathIndex + 1]
pipeExpression.node.body.splice(segmentIndexInPipe[0] as number, 1)
pipeExpression.body.splice(segmentIndexInPipe[0] as number, 1)
// Move up to the next segment.
segmentIndexInPipe[0] = Math.max((segmentIndexInPipe[0] as number) - 1, 0)
@ -902,19 +917,23 @@ export async function deleteFromSelection(
const astClone = structuredClone(ast)
const range = selection.range
const path = getNodePathFromSourceRange(ast, range)
const varDec = getNodeFromPath<VariableDeclarator>(
const _varDec = getNodeFromPath<VariableDeclarator>(
ast,
path,
'VariableDeclarator'
)
if (err(varDec)) return varDec
if (err(_varDec)) return _varDec
const { stopAtNode: varDec } = _varDec
if (!varDec) {
return new Error('VariableDeclarator not found')
}
if (
(selection.type === 'extrude-wall' ||
selection.type === 'end-cap' ||
selection.type === 'start-cap') &&
varDec.node.init.type === 'PipeExpression'
varDec.init.type === 'PipeExpression'
) {
const varDecName = varDec.node.id.name
const varDecName = varDec.id.name
let pathToNode: PathToNode | null = null
let extrudeNameToDelete = ''
traverse(astClone, {
@ -976,13 +995,16 @@ export async function deleteFromSelection(
lastKey: number
}[] = []
for (const { path, sketchName } of pathsDependingOnExtrude) {
const parent = getNodeFromPath<PipeExpression['body']>(
astClone,
path.slice(0, -1)
)
if (err(parent)) {
const _parent = getLastNodeFromPath(astClone, path.slice(0, -1))
if (err(_parent)) {
return
}
const { node: parent } = _parent
if (!isArray(parent)) {
console.error(`Parent is not an array: ${parent}`)
return
}
const pipeBodyItems = parent as PipeExpression['body']
const sketchToPreserve = programMemory.get(sketchName) as SketchGroup
console.log('sketchName', sketchName)
// Can't kick off multiple requests at once as getFaceDetails
@ -1000,7 +1022,7 @@ export async function deleteFromSelection(
}
const lastKey = Number(path.slice(-1)[0][0])
modificationDetails.push({
parent: parent.node,
parent: pipeBodyItems,
faceDetails,
lastKey,
})
@ -1048,14 +1070,14 @@ export async function deleteFromSelection(
}
// await prom
return astClone
} else if (varDec.node.init.type === 'PipeExpression') {
const pipeBody = varDec.node.init.body
} else if (varDec.init.type === 'PipeExpression') {
const pipeBody = varDec.init.body
if (
pipeBody[0].type === 'CallExpression' &&
pipeBody[0].callee.name === 'startSketchOn'
) {
// remove varDec
const varDecIndex = varDec.shallowPath[1][0] as number
const varDecIndex = _varDec.shallowPath[1][0] as number
astClone.body.splice(varDecIndex, 1)
return astClone
}

View File

@ -12,7 +12,7 @@ import {
hasValidFilletSelection,
isTagUsedInFillet,
} from './addFillet'
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { expectNodeOnPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap'
import { Selections } from 'lib/selections'
@ -270,13 +270,13 @@ const extrude001 = extrude(-5, sketch001)
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
const callExp = expectNodeOnPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
const edges = isTagUsedInFillet({ ast, callExp })
expect(edges).toEqual(['getOppositeEdge', 'baseEdge'])
})
it('should correctly identify getPreviousAdjacentEdge edges', () => {
@ -289,13 +289,13 @@ const extrude001 = extrude(-5, sketch001)
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
const callExp = expectNodeOnPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
const edges = isTagUsedInFillet({ ast, callExp })
expect(edges).toEqual(['getPreviousAdjacentEdge'])
})
it('should correctly identify no edges', () => {
@ -308,13 +308,13 @@ const extrude001 = extrude(-5, sketch001)
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
const callExp = expectNodeOnPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
const edges = isTagUsedInFillet({ ast, callExp })
expect(edges).toEqual([])
})
})

View File

@ -18,6 +18,7 @@ import {
createPipeExpression,
} from '../modifyAst'
import {
expectNodeOnPath,
getNodeFromPath,
getNodePathFromSourceRange,
hasSketchPipeBeenExtruded,
@ -46,15 +47,12 @@ export function addFillet(
*/
// Find the specific sketch segment to tag with the new tag
const sketchSegmentChunk = getNodeFromPath(
const sketchSegmentNode = expectNodeOnPath<CallExpression>(
_node,
pathToSegmentNode,
'CallExpression'
)
if (err(sketchSegmentChunk)) return sketchSegmentChunk
const { node: sketchSegmentNode } = sketchSegmentChunk as {
node: CallExpression
}
if (err(sketchSegmentNode)) return sketchSegmentNode
// Check whether selection is a valid segment from sketchLineHelpersMap
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) {
@ -93,13 +91,12 @@ export function addFillet(
])
// Locate the extrude call
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
const extrudeVarDecl = expectNodeOnPath<VariableDeclaration>(
_node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
const { node: extrudeVarDecl } = extrudeChunk
if (err(extrudeVarDecl)) return extrudeVarDecl
const extrudeDeclarator = extrudeVarDecl.declarations[0]
const extrudeInit = extrudeDeclarator.init
@ -281,12 +278,12 @@ export const hasValidFilletSelection = ({
'CallExpression'
)
if (err(segmentNode)) return false
if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name
if (segmentNode.stopAtNode) {
const segmentName = segmentNode.stopAtNode.callee.name
if (segmentName in sketchLineHelperMap) {
const edges = isTagUsedInFillet({
ast,
callExp: segmentNode.node,
callExp: segmentNode.stopAtNode,
})
// edge has already been filleted
if (

View File

@ -19,13 +19,47 @@ import {
} from './wasm'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
import { getAngle } from '../lib/utils'
import { getAngle, isArray } from '../lib/utils'
import { getFirstArg } from './std/sketch'
import {
getConstraintLevelFromSourceRange,
getConstraintType,
} from './std/sketchcombos'
import { err } from 'lib/trap'
import { BodyItem } from 'wasm-lib/kcl/bindings/BodyItem'
export interface DynamicNode {
type: SyntaxType
// Source range of the node.
start: number
end: number
[index: string]: unknown
}
function isAstNode(
node: any
): node is { type: SyntaxType; start: number; end: number } {
// TODO: Should we check for start and end also?
return node && typeof node === 'object' && 'type' in node
}
/**
* Given T and its corresponding SyntaxType, narrow the node to type T if
* node.type matches.
*/
export function isNodeType<T extends DynamicNode>(
node: unknown,
syntaxType: SyntaxType | SyntaxType[]
): node is T {
return (
!!node &&
typeof node === 'object' &&
'type' in node &&
(isArray(syntaxType)
? syntaxType.includes(node.type as SyntaxType)
: node.type === syntaxType)
)
}
/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -33,51 +67,64 @@ import { err } from 'lib/trap'
* and return the node at the end of this path.
* By default it will return the node of the deepest "stopAt" type encountered, or the node at the end of the path if no "stopAt" type is provided.
* If the "returnEarly" flag is set to true, the function will return as soon as a node of the specified type is found.
*
* If stopAt is provided, it must match T's type property.
*/
export function getNodeFromPath<T>(
export function getNodeFromPath<T extends DynamicNode>(
node: Program,
path: PathToNode,
stopAt?: SyntaxType | SyntaxType[],
returnEarly = false
):
| {
node: T
node: DynamicNode | unknown[]
stopAtNode: T | null
shallowPath: PathToNode
deepPath: PathToNode
}
| Error {
let currentNode = node as any
let stopAtNode = null
let currentNode: DynamicNode | unknown[] = node
let stopAtNode: T | null = null
let successfulPaths: PathToNode = []
let pathsExplored: PathToNode = []
for (const pathItem of path) {
if (typeof currentNode[pathItem[0]] !== 'object') {
if (stopAtNode) {
const pathIndex = pathItem[0]
let nextNode: unknown
if (currentNode && typeof pathIndex === 'number' && isArray(currentNode)) {
nextNode = currentNode[pathIndex]
}
if (
currentNode &&
typeof pathIndex === 'string' &&
typeof currentNode === 'object' &&
!isArray(currentNode)
) {
nextNode = currentNode[pathIndex]
}
if (!isArray(nextNode) && !isAstNode(nextNode)) {
if (isAstNode(stopAtNode)) {
return {
node: stopAtNode,
stopAtNode,
shallowPath: pathsExplored,
deepPath: successfulPaths,
}
}
return new Error('not an object')
}
currentNode = currentNode?.[pathItem[0]]
currentNode = nextNode
successfulPaths.push(pathItem)
if (!stopAtNode) {
pathsExplored.push(pathItem)
}
if (
typeof stopAt !== 'undefined' &&
(Array.isArray(stopAt)
? stopAt.includes(currentNode.type)
: currentNode.type === stopAt)
) {
if (stopAt && isNodeType<T>(currentNode, stopAt)) {
// it will match the deepest node of the type
// instead of returning at the first match
stopAtNode = currentNode
if (returnEarly) {
return {
node: stopAtNode,
stopAtNode,
shallowPath: pathsExplored,
deepPath: successfulPaths,
}
@ -86,32 +133,104 @@ export function getNodeFromPath<T>(
}
return {
node: stopAtNode || currentNode,
stopAtNode,
shallowPath: pathsExplored,
deepPath: successfulPaths,
}
}
/**
* Returns the terminal node in the given path. Like getNodeFromPath, but no
* stopAt parameter.
*/
export function getLastNodeFromPath(
node: Program,
path: PathToNode
):
| {
node: DynamicNode | unknown[]
}
| Error {
const _result = getNodeFromPath<DynamicNode>(node, path)
if (err(_result)) return _result
return {
node: _result.node,
}
}
/**
* Returns the terminal node in the given path, and asserts that it's the given
* type.
*/
export function expectLastNodeFromPath<T extends DynamicNode>(
node: Program,
path: PathToNode,
syntaxType: SyntaxType | SyntaxType[]
): T | Error {
const result = getLastNodeFromPath(node, path)
if (err(result)) return result
if (!isNodeType<T>(result.node, syntaxType)) {
return new Error(
`Expected node of type ${syntaxType}: found ${JSON.stringify(result)}`
)
}
return result.node
}
/**
* Returns the node in the path with the given type. Like getNodeFromPath, but
* if no stopAt node is found, an error is returned instead of null.
*
* @param {boolean} [options.firstFound=false] if true, return the first node of
* the type found. The default returns the last node found.
* @param {boolean} [options.message] the error message to return if the node is
* not found.
*/
export function expectNodeOnPath<T extends DynamicNode>(
node: Program,
path: PathToNode,
syntaxType: SyntaxType,
options: { firstFound?: boolean; message?: string } = {
firstFound: false,
}
): T | Error {
const result = getNodeFromPath<T>(node, path, syntaxType, options.firstFound)
if (err(result)) return result
if (!result.stopAtNode) {
return new Error(
options.message ?? `Node of type ${syntaxType} not found in path`
)
}
return result.stopAtNode
}
/**
* Functions the same as getNodeFromPath, but returns a curried function that can be called with the stopAt and returnEarly arguments.
*/
export function getNodeFromPathCurry(
node: Program,
path: PathToNode
): <T>(
stopAt?: SyntaxType | SyntaxType[],
): <T extends DynamicNode>(
stopAt: SyntaxType | SyntaxType[],
returnEarly?: boolean
) =>
| {
node: T
node: DynamicNode | unknown[]
stopAtNode: T | null
path: PathToNode
}
| Error {
return <T>(stopAt?: SyntaxType | SyntaxType[], returnEarly = false) => {
return <T extends DynamicNode>(
stopAt: SyntaxType | SyntaxType[],
returnEarly = false
) => {
const _node1 = getNodeFromPath<T>(node, path, stopAt, returnEarly)
if (err(_node1)) return _node1
const { node: _node, shallowPath } = _node1
const { node: _node, stopAtNode, shallowPath } = _node1
return {
node: _node,
stopAtNode,
path: shallowPath,
}
}
@ -459,7 +578,7 @@ export function findAllPreviousVariablesPath(
const { index: insertIndex, path: bodyPath } = splitPathAtLastIndex(pathToDec)
const _node2 = getNodeFromPath<Program['body']>(ast, bodyPath)
const _node2 = getLastNodeFromPath(ast, bodyPath)
if (err(_node2)) {
console.error(_node2)
return {
@ -468,19 +587,30 @@ export function findAllPreviousVariablesPath(
insertIndex: 0,
}
}
if (!isArray(_node2.node)) {
console.error(
`Expected node of type array, but found: ${JSON.stringify(_node2)}`
)
return {
variables: [],
bodyPath: [],
insertIndex: 0,
}
}
const { node: bodyItems } = _node2
const variables: PrevVariable<any>[] = []
bodyItems?.forEach?.((item) => {
if (item.type !== 'VariableDeclaration' || item.end > startRange) return
for (const unknownItem of bodyItems) {
const item = unknownItem as BodyItem
if (item.type !== 'VariableDeclaration' || item.end > startRange) continue
const varName = item.declarations[0].id.name
const varValue = programMemory?.get(varName)
if (!varValue || typeof varValue?.value !== type) return
if (!varValue || typeof varValue?.value !== type) continue
variables.push({
key: varName,
value: varValue.value,
})
})
}
return {
insertIndex,
@ -554,7 +684,7 @@ export function isNodeSafeToReplacePath(
}
pathToReplaced[1][0] = index + 1
const startPath = finPath.slice(0, -1)
const _nodeToReplace = getNodeFromPath(_ast, startPath)
const _nodeToReplace = getLastNodeFromPath(_ast, startPath)
if (err(_nodeToReplace)) return _nodeToReplace
const nodeToReplace = _nodeToReplace.node as any
nodeToReplace[last[0]] = identifier
@ -656,7 +786,8 @@ export function isLinesParallelAndConstrained(
'CallExpression'
)
if (err(_secondaryNode)) return _secondaryNode
const secondaryNode = _secondaryNode.node
const secondaryNode = _secondaryNode.stopAtNode
if (!secondaryNode) return new Error('no secondary node found')
const _varDec = getNodeFromPath(ast, primaryPath, 'VariableDeclaration')
if (err(_varDec)) return _varDec
const varDec = _varDec.node
@ -682,7 +813,7 @@ export function isLinesParallelAndConstrained(
Math.abs(primaryAngle - secondaryAngle) < EPSILON ||
Math.abs(primaryAngle - secondaryAngleAlt) < EPSILON
// is secordary line fully constrain, or has constrain type of 'angle'
// is secondary line fully constrain, or has constrain type of 'angle'
const secondaryFirstArg = getFirstArg(secondaryNode)
if (err(secondaryFirstArg)) return secondaryFirstArg
@ -747,8 +878,8 @@ export function doesPipeHaveCallExp({
console.error(pipeExpressionMeta)
return false
}
const pipeExpression = pipeExpressionMeta.node
if (pipeExpression.type !== 'PipeExpression') return false
const pipeExpression = pipeExpressionMeta.stopAtNode
if (!pipeExpression) return false
return pipeExpression.body.some(
(expression) =>
expression.type === 'CallExpression' &&
@ -775,8 +906,8 @@ export function hasExtrudeSketchGroup({
console.error(varDecMeta)
return false
}
const varDec = varDecMeta.node
if (varDec.type !== 'VariableDeclaration') return false
const varDec = varDecMeta.stopAtNode
if (!varDec) return false
const varName = varDec.declarations[0].id.name
const varValue = programMemory?.get(varName)
return varValue?.type === 'ExtrudeGroup' || varValue?.type === 'SketchGroup'
@ -814,8 +945,8 @@ export function findUsesOfTagInPipe(
console.error(nodeMeta)
return []
}
const node = nodeMeta.node
if (node.type !== 'CallExpression') return []
const node = nodeMeta.stopAtNode
if (!node) return []
const tagIndex = node.callee.name === 'close' ? 1 : 2
const thirdParam = node.arguments[tagIndex]
if (
@ -836,9 +967,14 @@ export function findUsesOfTagInPipe(
console.error(varDec)
return []
}
const varDecNode = varDec.stopAtNode
if (!varDecNode) {
console.error('varDecNode not found')
return []
}
const dependentRanges: SourceRange[] = []
traverse(varDec.node, {
traverse(varDecNode, {
enter: (node) => {
if (
node.type !== 'CallExpression' ||
@ -860,16 +996,16 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
const path = getNodePathFromSourceRange(ast, selection.range)
const _node = getNodeFromPath<PipeExpression>(ast, path, 'PipeExpression')
if (err(_node)) return false
const { node: pipeExpression } = _node
if (pipeExpression.type !== 'PipeExpression') return false
const { stopAtNode: pipeExpression } = _node
if (!pipeExpression) return false
const _varDec = getNodeFromPath<VariableDeclarator>(
ast,
path,
'VariableDeclarator'
)
if (err(_varDec)) return false
const varDec = _varDec.node
if (varDec.type !== 'VariableDeclarator') return false
const varDec = _varDec.stopAtNode
if (!varDec) return false
let extruded = false
traverse(ast as any, {
enter(node) {

View File

@ -2,7 +2,7 @@ import { Program, SourceRange } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL } from 'env'
import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave'
import { deferExecution, uuidv4 } from 'lib/utils'
import { uuidv4 } from 'lib/utils'
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import {
@ -12,7 +12,6 @@ import {
ResponseMap,
createArtifactMap,
} from 'lang/std/artifactMap'
import { useModelingContext } from 'hooks/useModelingContext'
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000
@ -1205,8 +1204,6 @@ export class EngineCommandManager extends EventTarget {
private onEngineConnectionNewTrack = ({
detail,
}: CustomEvent<NewTrackArgs>) => {}
modelingSend: ReturnType<typeof useModelingContext>['send'] =
(() => {}) as any
start({
restart,
@ -1552,6 +1549,7 @@ export class EngineCommandManager extends EventTarget {
}
}
async startNewSession() {
this.artifactMap = {}
this.orderedCommands = []
this.responseMap = {}
await this.initPlanes()
@ -1786,14 +1784,6 @@ export class EngineCommandManager extends EventTarget {
this.engineConnection?.send(message.command)
return promise
}
deferredArtifactPopulated = deferExecution((a?: null) => {
this.modelingSend({ type: 'Artifact graph populated' })
}, 200)
deferredArtifactEmptied = deferExecution((a?: null) => {
this.modelingSend({ type: 'Artifact graph emptied' })
}, 200)
/**
* When an execution takes place we want to wait until we've got replies for all of the commands
* When this is done when we build the artifact map synchronously.
@ -1805,16 +1795,21 @@ export class EngineCommandManager extends EventTarget {
responseMap: this.responseMap,
ast: this.getAst(),
})
if (Object.values(this.artifactMap).length) {
this.deferredArtifactEmptied(null)
} else {
this.deferredArtifactPopulated(null)
}
}
private async initPlanes() {
if (this.planesInitialized()) return
const planes = await this.makeDefaultPlanes()
this.defaultPlanes = planes
this.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) return
if (!planes) return
if (![planes.xy, planes.yz, planes.xz].includes(data.entity_id)) return
this.onPlaneSelectCallback(data.entity_id)
},
})
}
planesInitialized(): boolean {
return (
@ -1825,6 +1820,11 @@ export class EngineCommandManager extends EventTarget {
)
}
onPlaneSelectCallback = (id: string) => {}
onPlaneSelected(callback: (id: string) => void) {
this.onPlaneSelectCallback = callback
}
async setPlaneHidden(id: string, hidden: boolean) {
return await this.sendSceneCommand({
type: 'modeling_cmd_req',

View File

@ -14,7 +14,11 @@ import {
SourceRange,
CallExpression,
} from '../wasm'
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import {
expectNodeOnPath,
getNodeFromPath,
getNodePathFromSourceRange,
} from '../queryAst'
import { enginelessExecutor } from '../../lib/testHelpers'
import { err } from 'lib/trap'
@ -600,13 +604,13 @@ describe('testing getConstraintInfo', () => {
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<CallExpression>(
const callExp = expectNodeOnPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return callExp
const result = getConstraintInfo(callExp.node, code, pathToNode)
const result = getConstraintInfo(callExp, code, pathToNode)
expect(result).toEqual(expected)
})
})
@ -754,13 +758,13 @@ describe('testing getConstraintInfo', () => {
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<CallExpression>(
const callExp = expectNodeOnPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return callExp
const result = getConstraintInfo(callExp.node, code, pathToNode)
const result = getConstraintInfo(callExp, code, pathToNode)
expect(result).toEqual(expected)
})
})
@ -1110,14 +1114,14 @@ describe('testing getConstraintInfo', () => {
]
if (err(ast)) return ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const callExp = getNodeFromPath<CallExpression>(
const callExp = expectNodeOnPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return callExp
const result = getConstraintInfo(callExp.node, code, pathToNode)
const result = getConstraintInfo(callExp, code, pathToNode)
expect(result).toEqual(expected)
})
})

View File

@ -14,8 +14,10 @@ import {
Identifier,
} from 'lang/wasm'
import {
isNodeType,
expectLastNodeFromPath,
expectNodeOnPath,
getNodeFromPath,
getNodeFromPathCurry,
getNodePathFromSourceRange,
} from 'lang/queryAst'
import {
@ -50,7 +52,7 @@ import {
mutateObjExpProp,
findUniqueName,
} from 'lang/modifyAst'
import { roundOff, getLength, getAngle } from 'lib/utils'
import { roundOff, getLength, getAngle, isArray } from 'lib/utils'
import { err } from 'lib/trap'
import { perpendicularDistance } from 'sketch-helpers'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
@ -330,13 +332,12 @@ export const lineTo: SketchLineHelper = {
referencedSegment,
}) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression>(
const pipe = expectNodeOnPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta
if (err(pipe)) return pipe
const newVals: [Value, Value] = [
createLiteral(roundOff(to[0], 2)),
@ -373,7 +374,7 @@ export const lineTo: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath(_node, pathToNode)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
@ -382,8 +383,13 @@ export const lineTo: SketchLineHelper = {
createLiteral(to[1]),
])
mutateArrExp(callExpression.arguments?.[0], toArrExp) ||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
mutateArrExp(firstArg, toArrExp) ||
mutateObjExpProp(firstArg, toArrExp, 'to')
}
}
return {
modifiedAst: _node,
pathToNode,
@ -414,7 +420,7 @@ export const line: SketchLineHelper = {
spliceBetween,
}) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression | CallExpression>(
const nodeMeta = getNodeFromPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
@ -427,12 +433,16 @@ export const line: SketchLineHelper = {
'VariableDeclarator'
)
if (err(nodeMeta2)) return nodeMeta2
const { node: varDec } = nodeMeta2
const { stopAtNode: varDec } = nodeMeta2
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
if (spliceBetween && !createCallback && pipe.type === 'PipeExpression') {
if (
spliceBetween &&
!createCallback &&
isNodeType<PipeExpression>(pipe, 'PipeExpression')
) {
const callExp = createCallExpression('line', [
createArrayExpression([newXVal, newYVal]),
createPipeSubstitution(),
@ -455,7 +465,11 @@ export const line: SketchLineHelper = {
}
}
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
if (
replaceExisting &&
createCallback &&
isNodeType<PipeExpression>(pipe, 'PipeExpression')
) {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
const { callExp, valueUsedInTransform } = createCallback(
[newXVal, newYVal],
@ -477,7 +491,7 @@ export const line: SketchLineHelper = {
createArrayExpression([newXVal, newYVal]),
createPipeSubstitution(),
])
if (pipe.type === 'PipeExpression') {
if (isNodeType<PipeExpression>(pipe, 'PipeExpression')) {
pipe.body = [...pipe.body, callExp]
return {
modifiedAst: _node,
@ -488,6 +502,7 @@ export const line: SketchLineHelper = {
],
}
} else {
if (!varDec) return new Error('Variable declaration not found')
varDec.init = createPipeExpression([varDec.init, callExp])
}
return {
@ -497,7 +512,11 @@ export const line: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
@ -506,10 +525,15 @@ export const line: SketchLineHelper = {
createLiteral(roundOff(to[1] - from[1], 2)),
])
if (callExpression.arguments?.[0].type === 'ObjectExpression') {
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
} else {
mutateArrExp(callExpression.arguments?.[0], toArrExp)
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
if (firstArg.type === 'ObjectExpression') {
mutateObjExpProp(firstArg, toArrExp, 'to')
} else {
mutateArrExp(firstArg, toArrExp)
}
}
}
return {
modifiedAst: _node,
@ -531,10 +555,12 @@ export const line: SketchLineHelper = {
export const xLineTo: SketchLineHelper = {
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression>('PipeExpression')
if (err(_node1)) return _node1
const { node: pipe } = _node1
const pipe = expectLastNodeFromPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(pipe)) return pipe
const newVal = createLiteral(roundOff(to[0], 2))
@ -563,14 +589,24 @@ export const xLineTo: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const newX = createLiteral(roundOff(to[0], 2))
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newX
} else {
mutateObjExpProp(callExpression.arguments?.[0], newX, 'to')
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
const newX = createLiteral(roundOff(to[0], 2))
if (isLiteralArrayOrStatic(firstArg)) {
callExpression.arguments[0] = newX
} else {
mutateObjExpProp(firstArg, newX, 'to')
}
}
}
return {
modifiedAst: _node,
@ -592,10 +628,12 @@ export const xLineTo: SketchLineHelper = {
export const yLineTo: SketchLineHelper = {
add: ({ node, pathToNode, to, replaceExisting, createCallback }) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression>('PipeExpression')
if (err(_node1)) return _node1
const { node: pipe } = _node1
const pipe = expectLastNodeFromPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(pipe)) return pipe
const newVal = createLiteral(roundOff(to[1], 2))
@ -624,14 +662,24 @@ export const yLineTo: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const newY = createLiteral(roundOff(to[1], 2))
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newY
} else {
mutateObjExpProp(callExpression.arguments?.[0], newY, 'to')
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
const newY = createLiteral(roundOff(to[1], 2))
if (isLiteralArrayOrStatic(firstArg)) {
callExpression.arguments[0] = newY
} else {
mutateObjExpProp(firstArg, newY, 'to')
}
}
}
return {
modifiedAst: _node,
@ -653,10 +701,12 @@ export const yLineTo: SketchLineHelper = {
export const xLine: SketchLineHelper = {
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression>('PipeExpression')
if (err(_node1)) return _node1
const { node: pipe } = _node1
const pipe = expectNodeOnPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(pipe)) return pipe
const newVal = createLiteral(roundOff(to[0] - from[0], 2))
const firstArg = newVal
@ -684,14 +734,24 @@ export const xLine: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const newX = createLiteral(roundOff(to[0] - from[0], 2))
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newX
} else {
mutateObjExpProp(callExpression.arguments?.[0], newX, 'length')
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
const newX = createLiteral(roundOff(to[0] - from[0], 2))
if (isLiteralArrayOrStatic(firstArg)) {
callExpression.arguments[0] = newX
} else {
mutateObjExpProp(firstArg, newX, 'length')
}
}
}
return {
modifiedAst: _node,
@ -713,10 +773,12 @@ export const xLine: SketchLineHelper = {
export const yLine: SketchLineHelper = {
add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression>('PipeExpression')
if (err(_node1)) return _node1
const { node: pipe } = _node1
const pipe = expectNodeOnPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(pipe)) return pipe
const newVal = createLiteral(roundOff(to[1] - from[1], 2))
if (replaceExisting && createCallback) {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
@ -741,14 +803,24 @@ export const yLine: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const newY = createLiteral(roundOff(to[1] - from[1], 2))
if (isLiteralArrayOrStatic(callExpression.arguments?.[0])) {
callExpression.arguments[0] = newY
} else {
mutateObjExpProp(callExpression.arguments?.[0], newY, 'length')
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
const newY = createLiteral(roundOff(to[1] - from[1], 2))
if (isLiteralArrayOrStatic(firstArg)) {
callExpression.arguments[0] = newY
} else {
mutateObjExpProp(firstArg, newY, 'length')
}
}
}
return {
modifiedAst: _node,
@ -777,8 +849,11 @@ export const tangentialArcTo: SketchLineHelper = {
referencedSegment,
}) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression | CallExpression>('PipeExpression')
const _node1 = getNodeFromPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(_node1)) return _node1
const { node: pipe } = _node1
const _node2 = getNodeFromPath<VariableDeclarator>(
@ -787,12 +862,16 @@ export const tangentialArcTo: SketchLineHelper = {
'VariableDeclarator'
)
if (err(_node2)) return _node2
const { node: varDec } = _node2
const { stopAtNode: varDec } = _node2
const toX = createLiteral(roundOff(to[0], 2))
const toY = createLiteral(roundOff(to[1], 2))
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
if (
replaceExisting &&
createCallback &&
isNodeType<PipeExpression>(pipe, 'PipeExpression')
) {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
const { callExp, valueUsedInTransform } = createCallback(
[toX, toY],
@ -813,7 +892,7 @@ export const tangentialArcTo: SketchLineHelper = {
createArrayExpression([toX, toY]),
createPipeSubstitution(),
])
if (pipe.type === 'PipeExpression') {
if (isNodeType<PipeExpression>(pipe, 'PipeExpression')) {
pipe.body = [...pipe.body, newLine]
return {
modifiedAst: _node,
@ -824,6 +903,7 @@ export const tangentialArcTo: SketchLineHelper = {
],
}
} else {
if (!varDec) return new Error('Variable declaration not found')
varDec.init = createPipeExpression([varDec.init, newLine])
}
return {
@ -833,15 +913,23 @@ export const tangentialArcTo: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const x = createLiteral(roundOff(to[0], 2))
const y = createLiteral(roundOff(to[1], 2))
const firstArg = callExpression.arguments?.[0]
if (!mutateArrExp(firstArg, createArrayExpression([x, y]))) {
mutateObjExpProp(firstArg, createArrayExpression([x, y]), 'to')
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
if (!mutateArrExp(firstArg, createArrayExpression([x, y]))) {
mutateObjExpProp(firstArg, createArrayExpression([x, y]), 'to')
}
}
}
return {
modifiedAst: _node,
@ -909,10 +997,12 @@ export const angledLine: SketchLineHelper = {
referencedSegment,
}) => {
const _node = { ...node }
const getNode = getNodeFromPathCurry(_node, pathToNode)
const _node1 = getNode<PipeExpression>('PipeExpression')
if (err(_node1)) return _node1
const { node: pipe } = _node1
const pipe = expectNodeOnPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(pipe)) return pipe
const newAngleVal = createLiteral(roundOff(getAngle(from, to), 0))
const newLengthVal = createLiteral(roundOff(getLength(from, to), 2))
@ -947,7 +1037,11 @@ export const angledLine: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const angle = roundOff(getAngle(from, to), 0)
@ -956,10 +1050,16 @@ export const angledLine: SketchLineHelper = {
const angleLit = createLiteral(angle)
const lengthLit = createLiteral(lineLength)
const firstArg = callExpression.arguments?.[0]
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'length')
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
if (
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'length')
}
}
}
return {
@ -993,20 +1093,18 @@ export const angledLineOfXLength: SketchLineHelper = {
replaceExisting,
}) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression>(
const pipe = expectNodeOnPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
if (err(pipe)) return pipe
const varDec = expectNodeOnPath<VariableDeclarator>(
_node,
pathToNode,
'VariableDeclarator'
)
if (err(nodeMeta2)) return nodeMeta2
const { node: varDec } = nodeMeta2
if (err(varDec)) return varDec
const variableName = varDec.id.name
const sketch = previousProgramMemory?.get(variableName)
@ -1040,23 +1138,33 @@ export const angledLineOfXLength: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const angle = roundOff(getAngle(from, to), 0)
const xLength = roundOff(Math.abs(to[0] - from[0]), 2)
const firstArg = callExpression.arguments?.[0]
const adjustedXLength = isAngleLiteral(firstArg)
? Math.abs(xLength)
: xLength // todo make work for variable angle > 180
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
const adjustedXLength = isAngleLiteral(firstArg)
? Math.abs(xLength)
: xLength // todo make work for variable angle > 180
const angleLit = createLiteral(angle)
const lengthLit = createLiteral(adjustedXLength)
const angleLit = createLiteral(angle)
const lengthLit = createLiteral(adjustedXLength)
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'length')
if (
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'length')
}
}
}
return {
@ -1090,20 +1198,18 @@ export const angledLineOfYLength: SketchLineHelper = {
replaceExisting,
}) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression>(
const pipe = expectNodeOnPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
if (err(pipe)) return pipe
const varDec = expectNodeOnPath<VariableDeclarator>(
_node,
pathToNode,
'VariableDeclarator'
)
if (err(nodeMeta2)) return nodeMeta2
const { node: varDec } = nodeMeta2
if (err(varDec)) return varDec
const variableName = varDec.id.name
const sketch = previousProgramMemory?.get(variableName)
if (!sketch || sketch.type !== 'SketchGroup') {
@ -1137,23 +1243,33 @@ export const angledLineOfYLength: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const angle = roundOff(getAngle(from, to), 0)
const yLength = roundOff(to[1] - from[1], 2)
const firstArg = callExpression.arguments?.[0]
const adjustedYLength = isAngleLiteral(firstArg)
? Math.abs(yLength)
: yLength // todo make work for variable angle > 180
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
const adjustedYLength = isAngleLiteral(firstArg)
? Math.abs(yLength)
: yLength // todo make work for variable angle > 180
const angleLit = createLiteral(angle)
const lengthLit = createLiteral(adjustedYLength)
const angleLit = createLiteral(angle)
const lengthLit = createLiteral(adjustedYLength)
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'length')
if (
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'length')
}
}
}
return {
@ -1187,14 +1303,13 @@ export const angledLineToX: SketchLineHelper = {
referencedSegment,
}) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression>(
const pipe = expectNodeOnPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(nodeMeta)) return nodeMeta
if (err(pipe)) return pipe
const { node: pipe } = nodeMeta
const angle = createLiteral(roundOff(getAngle(from, to), 0))
const xArg = createLiteral(roundOff(to[0], 2))
if (replaceExisting && createCallback) {
@ -1227,22 +1342,32 @@ export const angledLineToX: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const angle = roundOff(getAngle(from, to), 0)
const xLength = roundOff(to[0], 2)
const firstArg = callExpression.arguments?.[0]
const adjustedXLength = xLength
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
const adjustedXLength = xLength
const angleLit = createLiteral(angle)
const lengthLit = createLiteral(adjustedXLength)
const angleLit = createLiteral(angle)
const lengthLit = createLiteral(adjustedXLength)
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'to')
if (
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'to')
}
}
}
return {
modifiedAst: _node,
@ -1275,14 +1400,12 @@ export const angledLineToY: SketchLineHelper = {
referencedSegment,
}) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression>(
const pipe = expectNodeOnPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta
if (err(pipe)) return pipe
const angle = createLiteral(roundOff(getAngle(from, to), 0))
const yArg = createLiteral(roundOff(to[1], 2))
@ -1317,22 +1440,32 @@ export const angledLineToY: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const angle = roundOff(getAngle(from, to), 0)
const xLength = roundOff(to[1], 2)
const firstArg = callExpression.arguments?.[0]
const adjustedXLength = xLength
if (isNodeType<CallExpression>(callExpression, 'CallExpression')) {
const firstArg = callExpression.arguments[0]
if (firstArg) {
const adjustedXLength = xLength
const angleLit = createLiteral(angle)
const lengthLit = createLiteral(adjustedXLength)
const angleLit = createLiteral(angle)
const lengthLit = createLiteral(adjustedXLength)
if (!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'to')
if (
!mutateArrExp(firstArg, createArrayExpression([angleLit, lengthLit]))
) {
mutateObjExpProp(firstArg, angleLit, 'angle')
mutateObjExpProp(firstArg, lengthLit, 'to')
}
}
}
return {
modifiedAst: _node,
@ -1372,7 +1505,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
)
if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta
const { stopAtNode: pipe } = nodeMeta
const angle = createLiteral(roundOff(getAngle(from, to), 0))
if (!referencedSegment) {
@ -1409,6 +1542,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
]
)
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
if (!pipe) return new Error('Pipe expression not found')
pipe.body[callIndex] = callExp
return {
modifiedAst: _node,
@ -1420,10 +1554,13 @@ export const angledLineThatIntersects: SketchLineHelper = {
},
updateArgs: ({ node, pathToNode, to, from, previousProgramMemory }) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
if (err(nodeMeta)) return nodeMeta
const callExpression = expectLastNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(callExpression)) return callExpression
const { node: callExpression } = nodeMeta
const angle = roundOff(getAngle(from, to), 0)
const firstArg = callExpression.arguments?.[0]
@ -1434,14 +1571,13 @@ export const angledLineThatIntersects: SketchLineHelper = {
: createLiteral('')
const intersectTagName =
intersectTag.type === 'Identifier' ? intersectTag.name : ''
const nodeMeta2 = getNodeFromPath<VariableDeclaration>(
const varDec = expectNodeOnPath<VariableDeclaration>(
_node,
pathToNode,
'VariableDeclaration'
)
if (err(nodeMeta2)) return nodeMeta2
if (err(varDec)) return varDec
const { node: varDec } = nodeMeta2
const varName = varDec.declarations[0].id.name
const sketchGroup = previousProgramMemory.get(varName) as SketchGroup
const intersectPath = sketchGroup.value.find(
@ -1553,11 +1689,16 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
to,
}) => {
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
if (err(nodeMeta)) {
console.error(nodeMeta)
const callExpression = expectLastNodeFromPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(callExpression)) {
console.error(callExpression)
return {
modifiedAst: {
type: 'Program',
start: 0,
end: 0,
body: [],
@ -1572,7 +1713,6 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
}
}
const { node: callExpression } = nodeMeta
const toArrExp = createArrayExpression([
createLiteral(roundOff(to[0])),
createLiteral(roundOff(to[1])),
@ -1615,8 +1755,14 @@ export function changeSketchArguments(
if (err(nodeMeta)) return nodeMeta
const { node: callExpression, shallowPath } = nodeMeta
if (isArray(callExpression)) {
return new Error('Expected call expression but found array')
}
if (!isNodeType<CallExpression>(callExpression, 'CallExpression')) {
return new Error('Call expression not found')
}
if (callExpression?.callee?.name in sketchLineHelperMap) {
if (callExpression.callee.name in sketchLineHelperMap) {
const { updateArgs } = sketchLineHelperMap[callExpression.callee.name]
if (!updateArgs) {
return new Error('not a sketch line helper')
@ -1631,7 +1777,7 @@ export function changeSketchArguments(
})
}
return new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
return new Error(`not a sketch line helper: ${callExpression.callee.name}`)
}
export function getConstraintInfo(
@ -1737,10 +1883,13 @@ export function addCallExpressionsToPipe({
)
if (err(pipeExpression)) return pipeExpression
if (pipeExpression.node.type !== 'PipeExpression') {
if (!pipeExpression.stopAtNode) {
return new Error('not a pipe expression')
}
pipeExpression.node.body = [...pipeExpression.node.body, ...expressions]
pipeExpression.stopAtNode.body = [
...pipeExpression.stopAtNode.body,
...expressions,
]
return _node
}
@ -1763,10 +1912,13 @@ export function addCloseToPipe({
)
if (err(pipeExpression)) return pipeExpression
if (pipeExpression.node.type !== 'PipeExpression') {
if (!pipeExpression.stopAtNode) {
return new Error('not a pipe expression')
}
pipeExpression.node.body = [...pipeExpression.node.body, closeExpression]
pipeExpression.stopAtNode.body = [
...pipeExpression.stopAtNode.body,
closeExpression,
]
return _node
}
@ -1854,18 +2006,16 @@ type addTagFn = (a: AddTagInfo) => { modifiedAst: Program; tag: string } | Error
function addTag(tagIndex = 2): addTagFn {
return ({ node, pathToNode }) => {
const _node = { ...node }
const callExpr = getNodeFromPath<CallExpression>(
const primaryCallExp = expectNodeOnPath<CallExpression>(
_node,
pathToNode,
'CallExpression'
)
if (err(callExpr)) return callExpr
const { node: primaryCallExp } = callExpr
if (err(primaryCallExp)) return primaryCallExp
// Tag is always 3rd expression now, using arg index feels brittle
// but we can come up with a better way to identify tag later.
const thirdArg = primaryCallExp.arguments?.[tagIndex]
const thirdArg = primaryCallExp.arguments[tagIndex]
const tagDeclarator =
thirdArg ||
(createTagDeclarator(findUniqueName(_node, 'seg', 2)) as TagDeclarator)

View File

@ -1,4 +1,4 @@
import { getNodeFromPath } from 'lang/queryAst'
import { getLastNodeFromPath } from 'lang/queryAst'
import { ToolTip, toolTips } from 'lang/langHelpers'
import {
Program,
@ -8,9 +8,9 @@ import {
SourceRange,
Path,
PathToNode,
Value,
} from '../wasm'
import { err } from 'lib/trap'
import { isArray } from 'lib/utils'
export function getSketchSegmentFromPathToNode(
sketchGroup: SketchGroup,
@ -25,11 +25,14 @@ export function getSketchSegmentFromPathToNode(
// TODO: once pathTodNode is stored on program memory as part of execution,
// we can check if the pathToNode matches the pathToNode of the sketchGroup.
// For now we fall back to the sourceRange
const nodeMeta = getNodeFromPath<Value>(ast, pathToNode)
const nodeMeta = getLastNodeFromPath(ast, pathToNode)
if (err(nodeMeta)) return nodeMeta
const { node } = nodeMeta
if (isArray(node)) {
return new Error('Value node expected, but found array')
}
const node = nodeMeta.node
if (!node || typeof node.start !== 'number' || !node.end)
if (typeof node.start !== 'number' || !node.end)
return new Error('no node found')
const sourceRange: SourceRange = [node.start, node.end]
return getSketchSegmentFromSourceRange(sketchGroup, sourceRange)

View File

@ -12,6 +12,9 @@ import {
ProgramMemory,
} from '../wasm'
import {
isNodeType,
expectNodeOnPath,
getLastNodeFromPath,
getNodeFromPath,
getNodeFromPathCurry,
getNodePathFromSourceRange,
@ -39,7 +42,7 @@ import {
getSketchSegmentFromPathToNode,
getSketchSegmentFromSourceRange,
} from './sketchConstraints'
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
import { getAngle, roundOff, normaliseAngle, isArray } from '../../lib/utils'
export type LineInputsType =
| 'xAbsolute'
@ -1225,22 +1228,19 @@ export function removeSingleConstraint({
objectProperty?: string
ast: Program
}): TransformInfo | false {
const callExp = getNodeFromPath<CallExpression>(
const callExp = expectNodeOnPath<CallExpression>(
ast,
pathToCallExp,
'CallExpression'
'CallExpression',
{ message: 'Invalid node type' }
)
if (err(callExp)) {
console.error(callExp)
return false
}
if (callExp.node.type !== 'CallExpression') {
console.error(new Error('Invalid node type'))
return false
}
const transform: TransformInfo = {
tooltip: callExp.node.callee.name as any,
tooltip: callExp.callee.name as any,
createNode:
({ tag, referenceSegName, varValues }) =>
(_, rawValues) => {
@ -1264,7 +1264,7 @@ export function removeSingleConstraint({
})
const objExp = createObjectExpression(expression)
return createStdlibCallExpression(
callExp.node.callee.name as any,
callExp.callee.name as any,
objExp,
tag
)
@ -1289,7 +1289,7 @@ export function removeSingleConstraint({
return varValue.value
})
return createStdlibCallExpression(
callExp.node.callee.name as any,
callExp.callee.name as any,
createArrayExpression(values),
tag
)
@ -1298,7 +1298,7 @@ export function removeSingleConstraint({
// if (typeof arrayIndex !== 'number' || !objectProperty) must be single value input xLine, yLineTo etc
return createCallWrapper(
callExp.node.callee.name as any,
callExp.callee.name as any,
rawValues[0].value,
tag
)
@ -1416,7 +1416,7 @@ export function getTransformInfos(
getNodePathFromSourceRange(ast, range)
)
const nodes = paths.map((pathToNode) =>
getNodeFromPath<Value>(ast, pathToNode, 'CallExpression')
getNodeFromPath<CallExpression>(ast, pathToNode, 'CallExpression')
)
try {
@ -1426,9 +1426,8 @@ export function getTransformInfos(
return false
}
const node = nodeMeta.node
if (node?.type === 'CallExpression')
return getTransformInfo(node, constraintType)
const node = nodeMeta.stopAtNode
if (node) return getTransformInfo(node, constraintType)
return false
}) as TransformInfo[]
@ -1448,9 +1447,7 @@ export function getRemoveConstraintsTransforms(
const paths = selectionRanges.codeBasedSelections.map((selectionRange) =>
getNodePathFromSourceRange(ast, selectionRange.range)
)
const nodes = paths.map((pathToNode) =>
getNodeFromPath<Value>(ast, pathToNode)
)
const nodes = paths.map((pathToNode) => getLastNodeFromPath(ast, pathToNode))
const theTransforms = nodes.map((nodeMeta) => {
// Typescript is not smart enough to know node will never be Error
@ -1461,7 +1458,11 @@ export function getRemoveConstraintsTransforms(
}
const node = nodeMeta.node
if (node?.type === 'CallExpression')
if (isArray(node)) {
console.error('Expected node, but found Array')
return false
}
if (isNodeType<CallExpression>(node, 'CallExpression'))
return getRemoveConstraintsTransform(node, constraintType)
return false
@ -1579,15 +1580,17 @@ export function transformAstSketchLines({
const callExp = getNode<CallExpression>('CallExpression')
if (err(callExp)) return callExp
if (!callExp.stopAtNode) return new Error('Call expression not found')
const varDec = getNode<VariableDeclarator>('VariableDeclarator')
if (err(varDec)) return varDec
if (!varDec.stopAtNode) return new Error('Variable declaration not found')
const firstArg = getFirstArg(callExp.node)
const firstArg = getFirstArg(callExp.stopAtNode)
if (err(firstArg)) return firstArg
const callBackTag = callExp.node.arguments[2]
const callBackTag = callExp.stopAtNode.arguments[2]
const _referencedSegmentNameVal =
callExp.node.arguments[0]?.type === 'ObjectExpression' &&
callExp.node.arguments[0].properties?.find(
callExp.stopAtNode.arguments[0]?.type === 'ObjectExpression' &&
callExp.stopAtNode.arguments[0].properties?.find(
(prop) => prop.key.name === 'intersectTag'
)?.value
const _referencedSegmentName =
@ -1601,7 +1604,7 @@ export function transformAstSketchLines({
const varValues: VarValues = []
getConstraintInfo(callExp.node, '', _pathToNode).forEach((a) => {
getConstraintInfo(callExp.stopAtNode, '', _pathToNode).forEach((a) => {
if (
a.type === 'tangentialWithPrevious' ||
a.type === 'horizontal' ||
@ -1609,33 +1612,46 @@ export function transformAstSketchLines({
)
return
const nodeMeta = getNodeFromPath<Value>(ast, a.pathToNode)
const nodeMeta = getLastNodeFromPath(ast, a.pathToNode)
if (err(nodeMeta)) return
// TODO: Assert that the node is a valid value.
if (a?.argPosition?.type === 'arrayItem') {
if (isArray(nodeMeta.node)) {
console.log('Expected Value, but found Array')
return
}
varValues.push({
type: 'arrayItem',
index: a.argPosition.index,
value: nodeMeta.node,
value: nodeMeta.node as Value,
argType: a.type,
})
} else if (a?.argPosition?.type === 'objectProperty') {
if (isArray(nodeMeta.node)) {
console.log('Expected Value, but found Array')
return
}
varValues.push({
type: 'objectProperty',
key: a.argPosition.key,
value: nodeMeta.node,
value: nodeMeta.node as Value,
argType: a.type,
})
} else if (a?.argPosition?.type === 'singleValue') {
if (isArray(nodeMeta.node)) {
console.log('Expected Value, but found Array')
return
}
varValues.push({
type: 'singleValue',
argType: a.type,
value: nodeMeta.node,
value: nodeMeta.node as Value,
})
}
})
const varName = varDec.node.id.name
const varName = varDec.stopAtNode.id.name
let sketchGroup = programMemory.get(varName)
if (sketchGroup?.type === 'ExtrudeGroup') {
sketchGroup = sketchGroup.sketchGroup
@ -1669,7 +1685,7 @@ export function transformAstSketchLines({
programMemory,
pathToNode: _pathToNode,
referencedSegment,
fnName: transformTo || (callExp.node.callee.name as ToolTip),
fnName: transformTo || (callExp.stopAtNode.callee.name as ToolTip),
to,
from,
createCallback: callBack({
@ -1743,15 +1759,14 @@ export function getConstraintLevelFromSourceRange(
ast: Program | Error
): Error | { range: [number, number]; level: ConstraintLevel } {
if (err(ast)) return ast
const nodeMeta = getNodeFromPath<CallExpression>(
const sketchFnExp = expectNodeOnPath<CallExpression>(
ast,
getNodePathFromSourceRange(ast, cursorRange),
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
if (err(sketchFnExp)) return sketchFnExp
const { node: sketchFnExp } = nodeMeta
const name = sketchFnExp?.callee?.name as ToolTip
const name = sketchFnExp.callee.name as ToolTip
const range: [number, number] = [sketchFnExp.start, sketchFnExp.end]
if (!toolTips.includes(name)) return { level: 'free', range: range }

View File

@ -5,15 +5,16 @@ import {
kclManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { CallExpression, SourceRange, Value, parse, recast } from 'lang/wasm'
import { CallExpression, SourceRange, parse, recast } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { uuidv4 } from 'lib/utils'
import { isArray, uuidv4 } from 'lib/utils'
import { EditorSelection, SelectionRange } from '@codemirror/state'
import { getNormalisedCoordinates, isOverlap } from 'lib/utils'
import { isCursorInSketchCommandRange } from 'lang/util'
import { Program } from 'lang/wasm'
import {
doesPipeHaveCallExp,
getLastNodeFromPath,
getNodeFromPath,
hasSketchPipeBeenExtruded,
isSingleCursorInPipe,
@ -177,7 +178,11 @@ export function getEventForSegmentSelection(
)
if (err(nodeMeta)) return null
const node = nodeMeta.node
const node = nodeMeta.stopAtNode
if (!node) {
console.error('Call expression not found')
return null
}
const range: SourceRange = [node.start, node.end]
return {
type: 'Set selection',
@ -300,7 +305,11 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
'CallExpression'
)
if (err(nodeMeta)) return
const node = nodeMeta.node
const node = nodeMeta.stopAtNode
if (!node) {
console.error('Call expression not found')
return
}
const groupHasCursor = codeBasedSelections.some((selection) => {
return isOverlap(selection.range, [node.start, node.end])
})
@ -597,9 +606,10 @@ export function updateSelections(
const newSelections = Object.entries(pathToNodeMap)
.map(([index, pathToNode]): Selection | undefined => {
const nodeMeta = getNodeFromPath<Value>(ast, pathToNode)
const nodeMeta = getLastNodeFromPath(ast, pathToNode)
if (err(nodeMeta)) return undefined
const node = nodeMeta.node
if (isArray(node)) return undefined
return {
range: [node.start, node.end],
type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type,

View File

@ -14,7 +14,6 @@ import {
listProjects,
readAppSettingsFile,
} from './tauri'
import { engineCommandManager } from './singletons'
export const isHidden = (fileOrDir: FileEntry) =>
!!fileOrDir.name?.startsWith('.')
@ -117,23 +116,9 @@ export async function getSettingsFolderPaths(projectPath?: string) {
}
}
export async function createAndOpenNewProject({
onProjectOpen,
navigate,
}: {
onProjectOpen: (
project: {
name: string | null
path: string | null
} | null,
file: FileEntry | null
) => void
export async function createAndOpenNewProject(
navigate: (path: string) => void
}) {
// Clear the scene and end the session.
engineCommandManager.endSession()
// Create a new project with the onboarding project name
) {
const configuration = await readAppSettingsFile()
const projects = await listProjects(configuration)
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
@ -141,24 +126,6 @@ export async function createAndOpenNewProject({
ONBOARDING_PROJECT_NAME,
nextIndex
)
const newProject = await createNewProjectDirectory(
name,
bracket,
configuration
)
// Prep the LSP and navigate to the onboarding start
onProjectOpen(
{
name: newProject.name,
path: newProject.path,
},
null
)
navigate(
`${paths.FILE}/${encodeURIComponent(newProject.default_file)}${
paths.ONBOARDING.INDEX
}`
)
return newProject
const newFile = await createNewProjectDirectory(name, bracket, configuration)
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
}

View File

@ -4,6 +4,13 @@ import { v4 } from 'uuid'
export const uuidv4 = v4
/**
* A safer type guard for arrays since the built-in Array.isArray() asserts `any[]`.
*/
export function isArray(val: any): val is unknown[] {
return Array.isArray(val)
}
export function isOverlap(a: SourceRange, b: SourceRange) {
const [startingRange, secondRange] = a[0] < b[0] ? [a, b] : [b, a]
const [lastOfFirst, firstOfSecond] = [startingRange[1], secondRange[0]]

File diff suppressed because one or more lines are too long

View File

@ -3,16 +3,22 @@ import { onboardingPaths } from 'routes/Onboarding/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme'
import { bracket } from 'lib/exampleKcl'
import { createAndOpenNewProject } from 'lib/tauriFS'
import {
getNextProjectIndex,
interpolateProjectNameWithIndex,
} from 'lib/tauriFS'
import { isTauri } from 'lib/isTauri'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons'
import { APP_NAME } from 'lib/constants'
import { useState } from 'react'
import { useLspContext } from 'components/LspProvider'
import { IndexLoaderData } from 'lib/types'
import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths'
import { useFileContext } from 'hooks/useFileContext'
import { codeManager, kclManager } from 'lib/singletons'
import { join } from '@tauri-apps/api/path'
import {
APP_NAME,
ONBOARDING_PROJECT_NAME,
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { createNewProjectDirectory, listProjects } from 'lib/tauri'
import { useState } from 'react'
/**
* Show either a welcome screen or a warning screen
@ -41,28 +47,30 @@ function OnboardingResetWarning(props: OnboardingResetWarningProps) {
{!isTauri() ? (
<OnboardingWarningWeb {...props} />
) : (
<OnboardingWarningDesktop {...props} />
<OnboardingWarningDesktop />
)}
</div>
</div>
)
}
function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
function OnboardingWarningDesktop() {
const navigate = useNavigate()
const dismiss = useDismiss()
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
const { context: fileContext } = useFileContext()
const { onProjectClose, onProjectOpen } = useLspContext()
async function onAccept() {
onProjectClose(
loaderData.file || null,
fileContext.project.path || null,
false
async function createAndOpenNewProject() {
const projects = await listProjects()
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
const name = interpolateProjectNameWithIndex(
ONBOARDING_PROJECT_NAME,
nextIndex
)
const newFile = await createNewProjectDirectory(name, bracket)
navigate(
`${paths.FILE}/${encodeURIComponent(
await join(newFile.path, PROJECT_ENTRYPOINT)
)}${paths.ONBOARDING.INDEX}`
)
await createAndOpenNewProject({ onProjectOpen, navigate })
props.setShouldShowWarning(false)
}
return (
@ -80,7 +88,11 @@ function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
<OnboardingButtons
className="mt-6"
dismiss={dismiss}
next={onAccept}
next={() => {
void createAndOpenNewProject()
codeManager.updateCodeEditor(bracket)
dismiss()
}}
nextText="Make a new project"
/>
</>

View File

@ -79,7 +79,7 @@ export const onboardingRoutes = [
export function useDemoCode() {
useEffect(() => {
if (!editorManager.editorView || codeManager.code === bracket) return
if (!editorManager.editorView) return
setTimeout(async () => {
codeManager.updateCodeStateEditor(bracket)
kclManager.isFirstRender = true

View File

@ -1437,7 +1437,6 @@ dependencies = [
"ts-rs",
"twenty-twenty",
"url",
"urlencoding",
"uuid",
"validator",
"wasm-bindgen",
@ -3439,12 +3438,6 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf-8"
version = "0.7.6"

View File

@ -37,6 +37,7 @@ fn basic() {
digest: None,
})],
non_code_meta: NonCodeMeta::default(),
ts_type: (),
digest: None,
};
assert_eq!(expected, actual);

View File

@ -42,7 +42,6 @@ thiserror = "1.0.63"
toml = "0.8.19"
ts-rs = { version = "9.0.1", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] }
urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40"

View File

@ -41,6 +41,17 @@ pub type Digest = [u8; 32];
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Program {
// This is so that it can be used with other AST nodes in TypeScript and the
// node type can be determined at runtime.
#[serde(
default,
rename = "type",
serialize_with = "Program::serialize_ts_type",
skip_deserializing
)]
#[ts(rename = "type", type = "\"Program\"")]
pub ts_type: (),
pub start: usize,
pub end: usize,
pub body: Vec<BodyItem>,
@ -82,6 +93,13 @@ impl Program {
hasher.update(slf.non_code_meta.compute_digest());
});
fn serialize_ts_type<S>(_: &(), serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str("Program")
}
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
// Check if we are in the non code meta.
if let Some(meta) = self.get_non_code_meta_for_position(pos) {
@ -1494,14 +1512,9 @@ impl CallExpression {
})?;
let result = result.ok_or_else(|| {
let mut source_ranges: Vec<SourceRange> = vec![self.into()];
// We want to send the source range of the original function.
if let MemoryItem::Function { meta, .. } = func {
source_ranges = meta.iter().map(|m| m.source_range).collect();
};
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of user-defined function {} is undefined", fn_name),
source_ranges,
source_ranges: vec![self.into()],
})
})?;
let result = result.get_value()?;
@ -5700,6 +5713,7 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
ts_type: (),
digest: None,
},
return_type: None,
@ -5728,6 +5742,7 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
ts_type: (),
digest: None,
},
return_type: None,
@ -5756,6 +5771,7 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
ts_type: (),
digest: None,
},
return_type: None,
@ -5797,6 +5813,7 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
ts_type: (),
digest: None,
},
return_type: None,

View File

@ -2735,22 +2735,6 @@ const bracket = startSketchOn('XY')
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_no_return() {
let ast = r#"fn test = (origin) => {
origin
}
test([0, 0])
"#;
let result = parse_execute(ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
r#"undefined value: KclErrorDetails { source_ranges: [SourceRange([10, 34])], message: "Result of user-defined function test is undefined" }"#.to_owned()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_doubly_nested_parens() {
let ast = r#"const sigmaAllow = 35000 // psi
@ -2926,6 +2910,7 @@ let w = f() + f()
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
ts_type: (),
digest: None,
},
return_type: None,

View File

@ -872,6 +872,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
end,
body,
non_code_meta,
ts_type: (),
digest: None,
})
}
@ -1746,6 +1747,7 @@ const mySk1 = startSketchAt([0, 0])"#;
}],
digest: None,
},
ts_type: (),
digest: None,
},
return_type: None,
@ -2400,6 +2402,7 @@ const mySk1 = startSketchAt([0, 0])"#;
digest: None,
})],
non_code_meta: NonCodeMeta::default(),
ts_type: (),
digest: None,
};
@ -2864,6 +2867,7 @@ e
digest: None,
})],
non_code_meta: NonCodeMeta::default(),
ts_type: (),
digest: None,
};

View File

@ -24,16 +24,13 @@ impl ProjectState {
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_from_path(path: PathBuf) -> Result<ProjectState> {
// Fix for "." path, which is the current directory.
let source_path = if path == Path::new(".") {
std::env::current_dir().map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
} else {
path
};
// Url decode the path.
let source_path =
std::path::Path::new(&urlencoding::decode(&source_path.display().to_string())?.to_string()).to_path_buf();
// If the path does not start with a slash, it is a relative path.
// We need to convert it to an absolute path.
let source_path = if source_path.is_relative() {
@ -1089,54 +1086,4 @@ const model = import("model.obj")"#
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_file_with_space_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("i have a space.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("i have a space.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("i have a space.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("i have a space.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_file_with_space_kcl_url_encoded() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("i have a space.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("i%20have%20a%20space.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("i have a space.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("i have a space.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
}