Compare commits

...

17 Commits

Author SHA1 Message Date
7b46656c0f Update docs 2024-08-02 21:07:41 -04:00
6c79b15adf Update existing tests 2024-08-02 21:07:41 -04:00
b45aa89d16 Require variable declaration for pipe expressions 2024-08-02 19:36:19 -04:00
834472e0a6 Update machine-api spec (#3244)
YOYO NEW API SPEC!

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-02 16:13:49 -07:00
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
43a34b191e Upgrade to clap 4.5.13 to fix build error (#3223) 2024-08-01 17:03:05 +00:00
49 changed files with 1752 additions and 559 deletions

View File

@ -15,7 +15,7 @@ close(sketch_group: SketchGroup, tag?: TagDeclarator) -> SketchGroup
### Examples ### Examples
```js ```js
startSketchOn('XZ') const exampleSketch = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([10, 10], %) |> line([10, 10], %)
|> line([10, 0], %) |> line([10, 0], %)

View File

@ -81459,7 +81459,7 @@
"unpublished": false, "unpublished": false,
"deprecated": false, "deprecated": false,
"examples": [ "examples": [
"startSketchOn('XZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)\n |> extrude(10, %)", "const exampleSketch = startSketchOn('XZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)\n |> extrude(10, %)",
"const exampleSketch = startSketchOn('-XZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 0], %)\n |> line([0, 10], %)\n |> close(%)\n\nconst example = extrude(10, exampleSketch)" "const exampleSketch = startSketchOn('-XZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 0], %)\n |> line([0, 10], %)\n |> close(%)\n\nconst example = extrude(10, exampleSketch)"
] ]
}, },

View File

@ -8117,7 +8117,7 @@ test('Typing KCL errors induces a badge on the error logs pane button', async ({
await u.closeDebugPanel() await u.closeDebugPanel()
// Ensure no badge is present // 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') await expect(errorLogsButton).not.toContainText('notification')
// Delete a character to break the KCL // 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", "name": "untitled-app",
"version": "0.24.6", "version": "0.24.7",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.17.0", "@codemirror/autocomplete": "^6.17.0",

View File

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

55
src-tauri/Cargo.lock generated
View File

@ -188,7 +188,7 @@ dependencies = [
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-updater", "tauri-plugin-updater",
"tokio", "tokio",
"toml 0.8.16", "toml 0.8.19",
"url", "url",
] ]
@ -727,7 +727,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
dependencies = [ dependencies = [
"serde", "serde",
"toml 0.8.16", "toml 0.8.19",
] ]
[[package]] [[package]]
@ -798,9 +798,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -808,9 +808,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -822,9 +822,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@ -1389,7 +1389,7 @@ dependencies = [
"cc", "cc",
"memchr", "memchr",
"rustc_version", "rustc_version",
"toml 0.8.16", "toml 0.8.19",
"vswhom", "vswhom",
"winreg 0.52.0", "winreg 0.52.0",
] ]
@ -2622,10 +2622,11 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"toml 0.8.16", "toml 0.8.19",
"tower-lsp", "tower-lsp",
"ts-rs", "ts-rs",
"url", "url",
"urlencoding",
"uuid", "uuid",
"validator", "validator",
"wasm-bindgen", "wasm-bindgen",
@ -5088,7 +5089,7 @@ dependencies = [
"cfg-expr", "cfg-expr",
"heck 0.5.0", "heck 0.5.0",
"pkg-config", "pkg-config",
"toml 0.8.16", "toml 0.8.19",
"version-compare", "version-compare",
] ]
@ -5241,7 +5242,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"tauri-winres", "tauri-winres",
"toml 0.8.16", "toml 0.8.19",
"walkdir", "walkdir",
] ]
@ -5299,7 +5300,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"toml 0.8.16", "toml 0.8.19",
"walkdir", "walkdir",
] ]
@ -5583,7 +5584,7 @@ dependencies = [
"serde_with", "serde_with",
"swift-rs", "swift-rs",
"thiserror", "thiserror",
"toml 0.8.16", "toml 0.8.19",
"url", "url",
"urlpattern", "urlpattern",
"walkdir", "walkdir",
@ -5830,21 +5831,21 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.16" version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_edit 0.22.17", "toml_edit 0.22.20",
] ]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.7" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -5886,15 +5887,15 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.17" version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [ dependencies = [
"indexmap 2.2.6", "indexmap 2.2.6",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow 0.6.6", "winnow 0.6.18",
] ]
[[package]] [[package]]
@ -6258,6 +6259,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "urlpattern" name = "urlpattern"
version = "0.2.0" version = "0.2.0"
@ -6965,9 +6972,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.6" version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View File

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

View File

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

View File

@ -190,49 +190,59 @@ export function Toolbar({
maybeIconConfig[0].onClick(configCallbackProps) maybeIconConfig[0].onClick(configCallbackProps)
} }
> >
<ToolbarItemContents <span
itemConfig={maybeIconConfig[0]} className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
configCallbackProps={configCallbackProps} >
/> {maybeIconConfig[0].title}
</span>
</ActionButton> </ActionButton>
<ToolbarItemTooltip
itemConfig={maybeIconConfig[0]}
configCallbackProps={configCallbackProps}
/>
</ActionButtonDropdown> </ActionButtonDropdown>
) )
} }
const itemConfig = maybeIconConfig const itemConfig = maybeIconConfig
return ( return (
<ActionButton <div className="relative" key={itemConfig.id}>
Element="button" <ActionButton
key={itemConfig.id} Element="button"
id={itemConfig.id} key={itemConfig.id}
data-testid={itemConfig.id} id={itemConfig.id}
iconStart={{ data-testid={itemConfig.id}
icon: itemConfig.icon, iconStart={{
className: iconClassName, icon: itemConfig.icon,
bgClassName: bgClassName, className: iconClassName,
}} bgClassName: bgClassName,
className={ }}
'pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' + className={
buttonBorderClassName + 'pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
' ' + buttonBorderClassName +
buttonBgClassName + ' ' +
(!itemConfig.showTitle ? ' !px-0' : '') buttonBgClassName +
} (!itemConfig.showTitle ? ' !px-0' : '')
name={itemConfig.title} }
aria-description={itemConfig.description} name={itemConfig.title}
aria-pressed={itemConfig.isActive} aria-description={itemConfig.description}
disabled={ aria-pressed={itemConfig.isActive}
disableAllButtons || disabled={
itemConfig.status !== 'available' || disableAllButtons ||
itemConfig.disabled itemConfig.status !== 'available' ||
} itemConfig.disabled
onClick={() => itemConfig.onClick(configCallbackProps)} }
> onClick={() => itemConfig.onClick(configCallbackProps)}
<ToolbarItemContents >
<span className={!itemConfig.showTitle ? 'sr-only' : ''}>
{itemConfig.title}
</span>
</ActionButton>
<ToolbarItemTooltip
itemConfig={itemConfig} itemConfig={itemConfig}
configCallbackProps={configCallbackProps} configCallbackProps={configCallbackProps}
/> />
</ActionButton> </div>
) )
})} })}
</ul> </ul>
@ -250,7 +260,7 @@ export function Toolbar({
* It contains a tooltip with the title, description, and links * It contains a tooltip with the title, description, and links
* and a hotkey listener * and a hotkey listener
*/ */
const ToolbarItemContents = memo(function ToolbarItemContents({ const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig, itemConfig,
configCallbackProps, configCallbackProps,
}: { }: {
@ -272,73 +282,69 @@ const ToolbarItemContents = memo(function ToolbarItemContents({
) )
return ( return (
<> <Tooltip
<span className={!itemConfig.showTitle ? 'sr-only' : ''}> inert={false}
{itemConfig.title} position="bottom"
</span> wrapperClassName="!p-4 !pointer-events-auto"
<Tooltip contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
position="bottom" >
wrapperClassName="!p-4 !pointer-events-auto" <div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch" <span
> className={`text-sm flex-1 ${
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50"> itemConfig.status !== 'available'
<span ? 'text-chalkboard-70 dark:text-chalkboard-40'
className={`text-sm flex-1 ${ : ''
itemConfig.status !== 'available' }`}
? 'text-chalkboard-70 dark:text-chalkboard-40' >
: '' {itemConfig.title}
}`} </span>
> {itemConfig.status === 'available' && itemConfig.hotkey ? (
{itemConfig.title} <kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd>
</span> ) : itemConfig.status === 'kcl-only' ? (
{itemConfig.status === 'available' && itemConfig.hotkey ? ( <>
<kbd className="flex-none hotkey">{itemConfig.hotkey}</kbd> <span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
) : itemConfig.status === 'kcl-only' ? ( 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"> <span className="text-wrap font-sans flex-0 text-chalkboard-70 dark:text-chalkboard-40">
KCL code only In development
</span> </span>
<CustomIcon <CustomIcon
name="code" name="lockClosed"
className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40" 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('mousedown', sceneInfra.onMouseDown, false)
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false) canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
sceneInfra.setSend(send) sceneInfra.setSend(send)
engineCommandManager.modelingSend = send
return () => { return () => {
canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove) canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove)
canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown) canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown)

View File

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

View File

@ -11,10 +11,8 @@ import {
Raycaster, Raycaster,
Vector2, Vector2,
Group, Group,
PlaneGeometry,
MeshBasicMaterial, MeshBasicMaterial,
Mesh, Mesh,
DoubleSide,
Intersection, Intersection,
Object3D, Object3D,
Object3DEventMap, Object3DEventMap,
@ -48,7 +46,6 @@ export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
export const DEBUG_SHOW_BOTH_SCENES: false = false export const DEBUG_SHOW_BOTH_SCENES: false = false
export const RAYCASTABLE_PLANE = 'raycastable-plane' export const RAYCASTABLE_PLANE = 'raycastable-plane'
export const DEFAULT_PLANES = 'default-planes'
export const X_AXIS = 'xAxis' export const X_AXIS = 'xAxis'
export const Y_AXIS = 'yAxis' export const Y_AXIS = 'yAxis'
@ -325,16 +322,9 @@ export class SceneInfra {
this.camControls.camera, this.camControls.camera,
this.camControls.target this.camControls.target
) )
const planesGroup = this.scene.getObjectByName(DEFAULT_PLANES)
const axisGroup = this.scene const axisGroup = this.scene
.getObjectByName(AXIS_GROUP) .getObjectByName(AXIS_GROUP)
?.getObjectByName('gridHelper') ?.getObjectByName('gridHelper')
planesGroup &&
planesGroup.scale.set(
scale / this._baseUnitMultiplier,
scale / this._baseUnitMultiplier,
scale / this._baseUnitMultiplier
)
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale) axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
} }
@ -632,59 +622,6 @@ export class SceneInfra {
this.onClickCallback({ mouseEvent, intersects }) 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[]) => { updateOtherSelectionColors = (otherSelections: Axis[]) => {
const axisGroup = this.scene.children.find( const axisGroup = this.scene.children.find(
({ userData }) => userData?.type === AXIS_GROUP ({ userData }) => userData?.type === AXIS_GROUP
@ -742,28 +679,3 @@ function baseUnitTomm(baseUnit: BaseUnit) {
return 914.4 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 { useLocation, useNavigate } from 'react-router-dom'
import { createAndOpenNewProject } from 'lib/tauriFS' import { createAndOpenNewProject } from 'lib/tauriFS'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { useLspContext } from './LspProvider'
const HelpMenuDivider = () => ( const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" /> <div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -12,16 +14,18 @@ const HelpMenuDivider = () => (
export function HelpMenu(props: React.PropsWithChildren) { export function HelpMenu(props: React.PropsWithChildren) {
const location = useLocation() const location = useLocation()
const { onProjectOpen } = useLspContext()
const filePath = useAbsoluteFilePath()
const isInProject = location.pathname.includes(paths.FILE) const isInProject = location.pathname.includes(paths.FILE)
const navigate = useNavigate() const navigate = useNavigate()
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
return ( return (
<Popover className="relative"> <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 <CustomIcon
name="questionMark" 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> <span className="sr-only">Help and resources</span>
<Tooltip position="top-right" wrapperClassName="ui-open:hidden"> <Tooltip position="top-right" wrapperClassName="ui-open:hidden">
@ -30,7 +34,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
</Popover.Button> </Popover.Button>
<Popover.Panel <Popover.Panel
as="ul" 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 <HelpMenuItem
as="a" as="a"
@ -84,7 +88,12 @@ export function HelpMenu(props: React.PropsWithChildren) {
</HelpMenuItem> </HelpMenuItem>
<HelpMenuItem <HelpMenuItem
as="button" 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 Keyboard shortcuts
</HelpMenuItem> </HelpMenuItem>
@ -99,9 +108,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
}, },
}) })
if (isInProject) { if (isInProject) {
navigate('onboarding') navigate(filePath + paths.ONBOARDING.INDEX)
} else { } else {
createAndOpenNewProject(navigate) createAndOpenNewProject({ onProjectOpen, navigate })
} }
}} }}
> >
@ -128,7 +137,7 @@ function HelpMenuItem({
}: HelpMenuItemProps) { }: HelpMenuItemProps) {
const baseClassName = 'block px-2 py-1 hover:bg-chalkboard-80' const baseClassName = 'block px-2 py-1 hover:bg-chalkboard-80'
return ( return (
<li className="m-0 p-0"> <li className="p-0 m-0">
{as === 'a' ? ( {as === 'a' ? (
<a <a
{...(props as React.ComponentProps<'a'>)} {...(props as React.ComponentProps<'a'>)}

View File

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

View File

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

View File

@ -19,7 +19,8 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { useDotDotSlash } from 'hooks/useDotDotSlash' import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { sep } from '@tauri-apps/api/path' 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 { interface AllSettingsFieldsProps {
searchParamTab: SettingsLevel searchParamTab: SettingsLevel
@ -33,9 +34,10 @@ export const AllSettingsFields = forwardRef(
) => { ) => {
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const { onProjectOpen } = useLspContext()
const dotDotSlash = useDotDotSlash() const dotDotSlash = useDotDotSlash()
const { const {
settings: { send, context }, settings: { send, context, state },
} = useSettingsAuthContext() } = useSettingsAuthContext()
const projectPath = const projectPath =
@ -48,19 +50,37 @@ export const AllSettingsFields = forwardRef(
) )
: undefined : undefined
function restartOnboarding() { async function restartOnboarding() {
send({ send({
type: `set.app.onboardingStatus`, type: `set.app.onboardingStatus`,
data: { level: 'user', value: '' }, 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 ( return (
<div className="relative overflow-y-auto"> <div className="relative overflow-y-auto">
<div ref={scrollRef} className="flex flex-col gap-4 px-2"> <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')) return
if (state.matches('Sketch no face')) return if (state.matches('idle.showPlanes')) return
if (!context.store?.didDragInStream && btnName(e).left) { if (!context.store?.didDragInStream && btnName(e).left) {
sendSelectEventToEngine( sendSelectEventToEngine(

View File

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

View File

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

View File

@ -1,10 +1,17 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { editorManager, engineCommandManager } from 'lib/singletons' import {
editorManager,
engineCommandManager,
kclManager,
sceneInfra,
} from 'lib/singletons'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { getEventForSelectWithPoint } from 'lib/selections' import { getEventForSelectWithPoint } from 'lib/selections'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { getNodePathFromSourceRange } from 'lang/queryAst'
export function useEngineConnectionSubscriptions() { export function useEngineConnectionSubscriptions() {
const { send, context } = useModelingContext() const { send, context, state } = useModelingContext()
useEffect(() => { useEffect(() => {
if (!engineCommandManager) return if (!engineCommandManager) return
@ -40,4 +47,135 @@ export function useEngineConnectionSubscriptions() {
unSubClick() unSubClick()
} }
}, [engineCommandManager, context?.sketchEnginePathId]) }, [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 { VITE_KC_API_WS_MODELING_URL } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { uuidv4 } from 'lib/utils' import { deferExecution, uuidv4 } from 'lib/utils'
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme' import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { import {
@ -12,6 +12,7 @@ import {
ResponseMap, ResponseMap,
createArtifactMap, createArtifactMap,
} from 'lang/std/artifactMap' } from 'lang/std/artifactMap'
import { useModelingContext } from 'hooks/useModelingContext'
// TODO(paultag): This ought to be tweakable. // TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000 const pingIntervalMs = 10000
@ -1204,6 +1205,8 @@ export class EngineCommandManager extends EventTarget {
private onEngineConnectionNewTrack = ({ private onEngineConnectionNewTrack = ({
detail, detail,
}: CustomEvent<NewTrackArgs>) => {} }: CustomEvent<NewTrackArgs>) => {}
modelingSend: ReturnType<typeof useModelingContext>['send'] =
(() => {}) as any
start({ start({
restart, restart,
@ -1549,7 +1552,6 @@ export class EngineCommandManager extends EventTarget {
} }
} }
async startNewSession() { async startNewSession() {
this.artifactMap = {}
this.orderedCommands = [] this.orderedCommands = []
this.responseMap = {} this.responseMap = {}
await this.initPlanes() await this.initPlanes()
@ -1784,6 +1786,14 @@ export class EngineCommandManager extends EventTarget {
this.engineConnection?.send(message.command) this.engineConnection?.send(message.command)
return promise 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 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. * When this is done when we build the artifact map synchronously.
@ -1795,21 +1805,16 @@ export class EngineCommandManager extends EventTarget {
responseMap: this.responseMap, responseMap: this.responseMap,
ast: this.getAst(), ast: this.getAst(),
}) })
if (Object.values(this.artifactMap).length) {
this.deferredArtifactEmptied(null)
} else {
this.deferredArtifactPopulated(null)
}
} }
private async initPlanes() { private async initPlanes() {
if (this.planesInitialized()) return if (this.planesInitialized()) return
const planes = await this.makeDefaultPlanes() const planes = await this.makeDefaultPlanes()
this.defaultPlanes = planes 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 { planesInitialized(): boolean {
return ( 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) { async setPlaneHidden(id: string, hidden: boolean) {
return await this.sendSceneCommand({ return await this.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',

View File

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

View File

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

View File

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

View File

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

View File

@ -1494,9 +1494,14 @@ impl CallExpression {
})?; })?;
let result = result.ok_or_else(|| { 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 { KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of user-defined function {} is undefined", fn_name), message: format!("Result of user-defined function {} is undefined", fn_name),
source_ranges: vec![self.into()], source_ranges,
}) })
})?; })?;
let result = result.get_value()?; let result = result.get_value()?;
@ -3989,7 +3994,7 @@ mod tests {
|> startProfileAt([0.0000000000, 5.0000000000], %) |> startProfileAt([0.0000000000, 5.0000000000], %)
|> line([0.4900857016, -0.0240763666], %) |> line([0.4900857016, -0.0240763666], %)
startSketchOn('XY') let s1 = startSketchOn('XY')
|> startProfileAt([0.0000000000, 5.0000000000], %) |> startProfileAt([0.0000000000, 5.0000000000], %)
|> line([0.4900857016, -0.0240763666], %) |> line([0.4900857016, -0.0240763666], %)
@ -4020,7 +4025,7 @@ ghi("things")
assert_eq!(folding_ranges[1].end_line, 254); assert_eq!(folding_ranges[1].end_line, 254);
assert_eq!( assert_eq!(
folding_ranges[1].collapsed_text, folding_ranges[1].collapsed_text,
Some("startSketchOn('XY')".to_string()) Some("let s1 = startSketchOn('XY')".to_string())
); );
assert_eq!(folding_ranges[2].start_line, 390); assert_eq!(folding_ranges[2].start_line, 390);
assert_eq!(folding_ranges[2].end_line, 403); assert_eq!(folding_ranges[2].end_line, 403);
@ -5259,7 +5264,7 @@ fn ghi = (part001) => {
#[test] #[test]
fn test_recast_trailing_comma() { fn test_recast_trailing_comma() {
let some_program_string = r#"startSketchOn('XY') let some_program_string = r#"let s = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> arc({ |> arc({
radius: 1, radius: 1,
@ -5273,7 +5278,7 @@ fn ghi = (part001) => {
let recasted = program.recast(&Default::default(), 0); let recasted = program.recast(&Default::default(), 0);
assert_eq!( assert_eq!(
recasted, recasted,
r#"startSketchOn('XY') r#"let s = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> arc({ |> arc({
radius: 1, radius: 1,
@ -5853,7 +5858,7 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_parse_tag_named_std_lib() { async fn test_parse_tag_named_std_lib() {
let some_program_string = r#"startSketchOn('XY') let some_program_string = r#"let s = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([5, 5], %, $xLine) |> line([5, 5], %, $xLine)
"#; "#;
@ -5864,13 +5869,13 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
assert!(result.is_err()); assert!(result.is_err());
assert_eq!( assert_eq!(
result.unwrap_err().to_string(), result.unwrap_err().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([76, 82])], message: "Cannot assign a tag to a reserved keyword: xLine" }"# r#"syntax: KclErrorDetails { source_ranges: [SourceRange([84, 90])], message: "Cannot assign a tag to a reserved keyword: xLine" }"#
); );
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_parse_empty_tag() { async fn test_parse_empty_tag() {
let some_program_string = r#"startSketchOn('XY') let some_program_string = r#"let s = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([5, 5], %, $) |> line([5, 5], %, $)
"#; "#;
@ -5881,13 +5886,13 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
assert!(result.is_err()); assert!(result.is_err());
assert_eq!( assert_eq!(
result.unwrap_err().to_string(), result.unwrap_err().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([57, 59])], message: "Unexpected token" }"# r#"syntax: KclErrorDetails { source_ranges: [SourceRange([65, 67])], message: "Unexpected token" }"#
); );
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_parse_digest() { async fn test_parse_digest() {
let prog1_string = r#"startSketchOn('XY') let prog1_string = r#"let s = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([5, 5], %) |> line([5, 5], %)
"#; "#;
@ -5895,7 +5900,7 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
let prog1_parser = crate::parser::Parser::new(prog1_tokens); let prog1_parser = crate::parser::Parser::new(prog1_tokens);
let prog1_digest = prog1_parser.ast().unwrap().compute_digest(); let prog1_digest = prog1_parser.ast().unwrap().compute_digest();
let prog2_string = r#"startSketchOn('XY') let prog2_string = r#"let s = startSketchOn('XY')
|> startProfileAt([0, 2], %) |> startProfileAt([0, 2], %)
|> line([5, 5], %) |> line([5, 5], %)
"#; "#;
@ -5905,7 +5910,7 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
assert!(prog1_digest != prog2_digest); assert!(prog1_digest != prog2_digest);
let prog3_string = r#"startSketchOn('XY') let prog3_string = r#"let s = startSketchOn('XY')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> line([5, 5], %) |> line([5, 5], %)
"#; "#;

View File

@ -2086,6 +2086,20 @@ const newVar = myVar + 1"#;
); );
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_top_level_pipe_without_variable() {
let ast = r#"startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> lineTo([2, 2], %, $yo)
"#;
let result = parse_execute(ast).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 78])], message: "A top-level pipe expression must be assigned to a new variable declaration" }"#.to_owned()
);
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_execute_angled_line_that_intersects() { async fn test_execute_angled_line_that_intersects() {
let ast_fn = |offset: &str| -> String { let ast_fn = |offset: &str| -> String {
@ -2735,6 +2749,22 @@ const bracket = startSketchOn('XY')
parse_execute(ast).await.unwrap(); 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")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_doubly_nested_parens() { async fn test_math_doubly_nested_parens() {
let ast = r#"const sigmaAllow = 35000 // psi let ast = r#"const sigmaAllow = 35000 // psi

View File

@ -1354,7 +1354,7 @@ async fn test_kcl_lsp_formatting() {
uri: "file:///test.kcl".try_into().unwrap(), uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(), language_id: "kcl".to_string(),
version: 1, version: 1,
text: r#"startSketchOn('XY') text: r#"let s = startSketchOn('XY')
|> startProfileAt([0,0], %)"# |> startProfileAt([0,0], %)"#
.to_string(), .to_string(),
}, },
@ -1385,7 +1385,7 @@ async fn test_kcl_lsp_formatting() {
assert_eq!(formatting.len(), 1); assert_eq!(formatting.len(), 1);
assert_eq!( assert_eq!(
formatting[0].new_text, formatting[0].new_text,
r#"startSketchOn('XY') r#"let s = startSketchOn('XY')
|> startProfileAt([0, 0], %)"# |> startProfileAt([0, 0], %)"#
); );
} }
@ -2901,7 +2901,7 @@ async fn test_kcl_lsp_folding() {
uri: "file:///test.kcl".try_into().unwrap(), uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(), language_id: "kcl".to_string(),
version: 1, version: 1,
text: r#"startSketchOn('XY') text: r#"let s = startSketchOn('XY')
|> startProfileAt([0,0], %)"# |> startProfileAt([0,0], %)"#
.to_string(), .to_string(),
}, },
@ -2926,12 +2926,12 @@ async fn test_kcl_lsp_folding() {
assert_eq!( assert_eq!(
folding.first().unwrap().clone(), folding.first().unwrap().clone(),
tower_lsp::lsp_types::FoldingRange { tower_lsp::lsp_types::FoldingRange {
start_line: 19, start_line: 27,
start_character: None, start_character: None,
end_line: 67, end_line: 75,
end_character: None, end_character: None,
kind: Some(tower_lsp::lsp_types::FoldingRangeKind::Region), kind: Some(tower_lsp::lsp_types::FoldingRangeKind::Region),
collapsed_text: Some("startSketchOn('XY')".to_string()) collapsed_text: Some("let s = startSketchOn('XY')".to_string())
} }
); );
} }

View File

@ -50,6 +50,37 @@ fn program(i: TokenSlice) -> PResult<Program> {
// Once this is merged and stable, consider changing this as I think it's more accurate // Once this is merged and stable, consider changing this as I think it's more accurate
// without the -1. // without the -1.
out.end -= 1; out.end -= 1;
// Prevent top-level pipe expressions without a variable declaration, giving
// a good error message that will help users fix their code. This is a
// band-aid until we can use the artifact graph.
let source_ranges = out
.body
.iter()
.filter_map(|item| {
if let BodyItem::ExpressionStatement(ExpressionStatement {
expression: Value::PipeExpression(_),
start,
end,
..
}) = item
{
Some(SourceRange([*start, *end]))
} else {
None
}
})
.collect::<Vec<_>>();
if !source_ranges.is_empty() {
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges,
message: "A top-level pipe expression must be assigned to a new variable declaration".to_owned(),
})
.into(),
));
}
Ok(out) Ok(out)
} }

View File

@ -24,13 +24,16 @@ impl ProjectState {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub async fn new_from_path(path: PathBuf) -> Result<ProjectState> { pub async fn new_from_path(path: PathBuf) -> Result<ProjectState> {
// Fix for "." path, which is the current directory. // Fix for "." path, which is the current directory.
let source_path = if path == Path::new(".") { let source_path = if path == Path::new(".") {
std::env::current_dir().map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))? std::env::current_dir().map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
} else { } else {
path 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. // If the path does not start with a slash, it is a relative path.
// We need to convert it to an absolute path. // We need to convert it to an absolute path.
let source_path = if source_path.is_relative() { 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(); 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();
}
} }

View File

@ -1395,7 +1395,7 @@ pub async fn close(args: Args) -> Result<MemoryItem, KclError> {
/// Close the current sketch. /// Close the current sketch.
/// ///
/// ```no_run /// ```no_run
/// startSketchOn('XZ') /// const exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %) /// |> startProfileAt([0, 0], %)
/// |> line([10, 10], %) /// |> line([10, 10], %)
/// |> line([10, 0], %) /// |> line([10, 0], %)

View File

@ -484,7 +484,7 @@ const part = roundedRectangle([0, 0], 20, 20, 4)
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn kcl_test_top_level_expression() { async fn kcl_test_top_level_expression() {
let code = r#"startSketchOn('XY') |> circle([0,0], 22, %) |> extrude(14, %)"#; let code = r#"let c1 = startSketchOn('XY') |> circle([0,0], 22, %) |> extrude(14, %)"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
assert_out("top_level_expression", &result); assert_out("top_level_expression", &result);
@ -2117,7 +2117,7 @@ async fn kcl_test_extrude_custom_plane() {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn kcl_test_arc_error_same_start_end() { async fn kcl_test_arc_error_same_start_end() {
let code = r#"startSketchOn('XY') let code = r#"let x = startSketchOn('XY')
|> startProfileAt([10, 0], %) |> startProfileAt([10, 0], %)
|> arc({ |> arc({
angle_start: 180, angle_start: 180,
@ -2137,7 +2137,7 @@ async fn kcl_test_arc_error_same_start_end() {
assert!(result.is_err()); assert!(result.is_err());
assert_eq!( assert_eq!(
result.err().unwrap().to_string(), result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([57, 140])], message: "Arc start and end angles must be different" }"# r#"type: KclErrorDetails { source_ranges: [SourceRange([65, 148])], message: "Arc start and end angles must be different" }"#
); );
} }