Compare commits

...

12 Commits

Author SHA1 Message Date
bcdf6e314f Cut release v0.24.7 (#3243) 2024-08-02 18:12:58 -04:00
55e9845ade Update machine-api spec (#3242)
YOYO NEW API SPEC!

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-02 14:10:52 -07:00
d61cf882c1 show default planes on empty scene (#3237)
* show default planes on empty sceen

* fmt

* remove log

* fix silly click listener bug

* delete old stuff

* test tweak

* Revert "test tweak"

This reverts commit e9cb4ac4b5.

---------

Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
2024-08-02 14:05:35 -07:00
874d19cbfd Re-get the openPanes from localStorage when navigating between projects (#3241)
* Re-get the openPanes from localStorage when navigating between projects

* fmt
2024-08-02 15:39:05 -04:00
9dcc955760 Regression fix: restarting onboarding in desktop app required two attempts (#3240)
* Fixed onboarding modal issue, revealed race

* Remove logs

* Make common reset onboarding code path
2024-08-02 15:38:39 -04:00
9b594efe53 Have links clickable within tooltips without clicking content below them (#3204)
* Have links clickable within tooltips without clicking content below them

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Re-run CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Re-run CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-02 12:25:57 -04:00
7b9f40c4cb Fix link to keybindings tab in help menu on Windows (#3236) 2024-08-02 10:25:42 -04:00
81b79da90f fix cryptic error (#3234)
* fix cryptic error

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update types.rs

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-01 19:40:22 -07:00
2ad5a880fa rm error pane show badge on code (#3233)
* rm error pane show badge on code

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix playwirght

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* empty

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-01 19:40:16 -07:00
b57a9ba54c open file with url encoded space (#3231)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-01 17:53:42 -07:00
b32f5c1d4e add html report to playwright artifact (#3229)
add htlm report to playwright artifact
2024-08-01 22:09:40 +00:00
b6d4cc7a4e Update machine-api spec (#3226)
YOYO NEW API SPEC!

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-01 14:49:01 -07:00
43 changed files with 1650 additions and 512 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 Errors pane' })
const errorLogsButton = page.getByRole('button', { name: 'KCL Code 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: 47 KiB

After

Width:  |  Height:  |  Size: 45 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: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 45 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: 35 KiB

After

Width:  |  Height:  |  Size: 34 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: 34 KiB

After

Width:  |  Height:  |  Size: 33 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.6",
"version": "0.24.7",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",

View File

@ -23,6 +23,7 @@ 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,6 +2626,7 @@ dependencies = [
"tower-lsp",
"ts-rs",
"url",
"urlencoding",
"uuid",
"validator",
"wasm-bindgen",
@ -6258,6 +6259,12 @@ 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.6"
"version": "0.24.7"
}

View File

@ -95,16 +95,16 @@ export function App() {
})
const newCmdId = uuidv4()
if (context.store?.buttonDownInStream === undefined) {
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})
}
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,
})
}
return (

View File

@ -190,49 +190,59 @@ export function Toolbar({
maybeIconConfig[0].onClick(configCallbackProps)
}
>
<ToolbarItemContents
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
/>
<span
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
>
{maybeIconConfig[0].title}
</span>
</ActionButton>
<ToolbarItemTooltip
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
/>
</ActionButtonDropdown>
)
}
const itemConfig = maybeIconConfig
return (
<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
<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
itemConfig={itemConfig}
configCallbackProps={configCallbackProps}
/>
</ActionButton>
</div>
)
})}
</ul>
@ -250,7 +260,7 @@ export function Toolbar({
* It contains a tooltip with the title, description, and links
* and a hotkey listener
*/
const ToolbarItemContents = memo(function ToolbarItemContents({
const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig,
configCallbackProps,
}: {
@ -272,73 +282,69 @@ const ToolbarItemContents = memo(function ToolbarItemContents({
)
return (
<>
<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' ? (
<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="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
KCL code only
In development
</span>
<CustomIcon
name="code"
name="lockClosed"
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>
</>
)
)}
</Tooltip>
</>
</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>
)
})

View File

@ -102,6 +102,7 @@ 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)

View File

@ -22,9 +22,6 @@ import {
import {
ARROWHEAD,
AXIS_GROUP,
DEFAULT_PLANES,
DefaultPlane,
defaultPlaneColor,
getSceneScale,
INTERSECTION_PLANE_LAYER,
OnClickCallbackArgs,
@ -202,6 +199,7 @@ export class SceneEntities {
createIntersectionPlane() {
if (sceneInfra.scene.getObjectByName(RAYCASTABLE_PLANE)) {
// this.removeIntersectionPlane()
console.warn('createIntersectionPlane called when it already exists')
return
}
@ -1502,146 +1500,6 @@ 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) => {

View File

@ -11,10 +11,8 @@ import {
Raycaster,
Vector2,
Group,
PlaneGeometry,
MeshBasicMaterial,
Mesh,
DoubleSide,
Intersection,
Object3D,
Object3DEventMap,
@ -48,7 +46,6 @@ 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'
@ -325,16 +322,9 @@ 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)
}
@ -632,59 +622,6 @@ 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
@ -742,28 +679,3 @@ 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,6 +5,8 @@ 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" />
@ -12,16 +14,18 @@ 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="border-none p-0 m-0 rounded-full grid place-content-center">
<Popover.Button className="grid p-0 m-0 border-none rounded-full place-content-center">
<CustomIcon
name="questionMark"
className="w-7 h-7 rounded-full bg-chalkboard-110 dark:bg-chalkboard-80 text-chalkboard-10"
className="rounded-full w-7 h-7 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">
@ -30,7 +34,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
</Popover.Button>
<Popover.Panel
as="ul"
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"
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"
>
<HelpMenuItem
as="a"
@ -84,7 +88,12 @@ export function HelpMenu(props: React.PropsWithChildren) {
</HelpMenuItem>
<HelpMenuItem
as="button"
onClick={() => navigate('settings?tab=keybindings')}
onClick={() => {
const targetPath = location.pathname.includes(paths.FILE)
? filePath + paths.SETTINGS
: paths.HOME + paths.SETTINGS
navigate(targetPath + '?tab=keybindings')
}}
>
Keyboard shortcuts
</HelpMenuItem>
@ -99,9 +108,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
},
})
if (isInProject) {
navigate('onboarding')
navigate(filePath + paths.ONBOARDING.INDEX)
} else {
createAndOpenNewProject(navigate)
createAndOpenNewProject({ onProjectOpen, navigate })
}
}}
>
@ -128,7 +137,7 @@ function HelpMenuItem({
}: HelpMenuItemProps) {
const baseClassName = 'block px-2 py-1 hover:bg-chalkboard-80'
return (
<li className="m-0 p-0">
<li className="p-0 m-0">
{as === 'a' ? (
<a
{...(props as React.ComponentProps<'a'>)}

View File

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

View File

@ -27,27 +27,3 @@ 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,7 +3,6 @@ import {
faBugSlash,
faCode,
faCodeCommit,
faExclamationCircle,
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu'
@ -11,7 +10,7 @@ import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
import { ReactNode } from 'react'
import { MemoryPane, MemoryPaneMenu } from './MemoryPane'
import { KclErrorsPane, LogsPane } from './LoggingPanes'
import { LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
import { useKclContext } from 'lang/KclProvider'
@ -21,7 +20,6 @@ export type SidebarType =
| 'debug'
| 'export'
| 'files'
| 'kclErrors'
| 'logs'
| 'lspMessages'
| 'variables'
@ -53,6 +51,7 @@ export const sidebarPanes: SidebarPane[] = [
Content: KclEditorPane,
keybinding: 'Shift + C',
Menu: KclEditorMenu,
showBadge: ({ kclContext }) => kclContext.errors.length,
},
{
id: 'files',
@ -78,14 +77,6 @@ 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,7 +19,8 @@ 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 } from 'react'
import { ForwardedRef, forwardRef, useEffect } from 'react'
import { useLspContext } from 'components/LspProvider'
interface AllSettingsFieldsProps {
searchParamTab: SettingsLevel
@ -33,9 +34,10 @@ export const AllSettingsFields = forwardRef(
) => {
const location = useLocation()
const navigate = useNavigate()
const { onProjectOpen } = useLspContext()
const dotDotSlash = useDotDotSlash()
const {
settings: { send, context },
settings: { send, context, state },
} = useSettingsAuthContext()
const projectPath =
@ -48,19 +50,37 @@ export const AllSettingsFields = forwardRef(
)
: undefined
function restartOnboarding() {
async function restartOnboarding() {
send({
type: `set.app.onboardingStatus`,
data: { level: 'user', value: '' },
})
if (isFileSettings) {
navigate(dotDotSlash(1) + paths.ONBOARDING.INDEX)
} else {
createAndOpenNewProject(navigate)
}
}
/**
* 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 })
}
}
}
navigateToOnboardingStart()
}, [isFileSettings, navigate, state])
return (
<div className="relative overflow-y-auto">
<div ref={scrollRef} className="flex flex-col gap-4 px-2">

View File

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

View File

@ -57,7 +57,8 @@
transition-delay: var(--_delay);
}
:is(:focus-visible) > .tooltipWrapper.withFocus {
:is(:focus-visible) > .tooltipWrapper.withFocus,
:focus-within > .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}
{...{ inert: inert ? '' : undefined }}
role="tooltip"
className={`p-3 ${
position !== 'left' && position !== 'right' ? 'px-0' : ''

View File

@ -1,10 +1,17 @@
import { useEffect } from 'react'
import { editorManager, engineCommandManager } from 'lib/singletons'
import {
editorManager,
engineCommandManager,
kclManager,
sceneInfra,
} 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 } = useModelingContext()
const { send, context, state } = useModelingContext()
useEffect(() => {
if (!engineCommandManager) return
@ -40,4 +47,135 @@ 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

@ -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 { uuidv4 } from 'lib/utils'
import { deferExecution, uuidv4 } from 'lib/utils'
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import {
@ -12,6 +12,7 @@ import {
ResponseMap,
createArtifactMap,
} from 'lang/std/artifactMap'
import { useModelingContext } from 'hooks/useModelingContext'
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000
@ -1204,6 +1205,8 @@ export class EngineCommandManager extends EventTarget {
private onEngineConnectionNewTrack = ({
detail,
}: CustomEvent<NewTrackArgs>) => {}
modelingSend: ReturnType<typeof useModelingContext>['send'] =
(() => {}) as any
start({
restart,
@ -1549,7 +1552,6 @@ export class EngineCommandManager extends EventTarget {
}
}
async startNewSession() {
this.artifactMap = {}
this.orderedCommands = []
this.responseMap = {}
await this.initPlanes()
@ -1784,6 +1786,14 @@ 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.
@ -1795,21 +1805,16 @@ 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 (
@ -1820,11 +1825,6 @@ 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,6 +14,7 @@ import {
listProjects,
readAppSettingsFile,
} from './tauri'
import { engineCommandManager } from './singletons'
export const isHidden = (fileOrDir: FileEntry) =>
!!fileOrDir.name?.startsWith('.')
@ -116,9 +117,23 @@ export async function getSettingsFolderPaths(projectPath?: string) {
}
}
export async function createAndOpenNewProject(
export async function createAndOpenNewProject({
onProjectOpen,
navigate,
}: {
onProjectOpen: (
project: {
name: string | null
path: string | null
} | null,
file: FileEntry | null
) => void
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)
@ -126,6 +141,24 @@ export async function createAndOpenNewProject(
ONBOARDING_PROJECT_NAME,
nextIndex
)
const newFile = await createNewProjectDirectory(name, bracket, configuration)
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
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
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -1437,6 +1437,7 @@ dependencies = [
"ts-rs",
"twenty-twenty",
"url",
"urlencoding",
"uuid",
"validator",
"wasm-bindgen",
@ -3438,6 +3439,12 @@ 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

@ -42,6 +42,7 @@ 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

@ -1494,9 +1494,14 @@ 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: vec![self.into()],
source_ranges,
})
})?;
let result = result.get_value()?;

View File

@ -2735,6 +2735,22 @@ 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

View File

@ -24,13 +24,16 @@ 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() {
@ -1086,4 +1089,54 @@ 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();
}
}