Compare commits

...

13 Commits

Author SHA1 Message Date
caddac5059 Bump to v0.7.1 (#548) 2023-09-15 15:43:48 -04:00
54751aa7bb Fix rtc_freeze_count error (#544) 2023-09-15 12:05:46 -04:00
7b7d5e5f5e Fix extrude w/o pipe (#542) 2023-09-15 11:51:39 -04:00
f7971bddef Rename TooTip to ToolTip (#541) 2023-09-15 11:48:23 -04:00
e4f2e66029 inital rework of execution (#528)
* inital rework

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

* updates

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

* update the program memory as well

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

* cleanups

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

* code

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

* updates for typing code

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

* fixing

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

* some fixes

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

* more fixes

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

* Only unselect line or move tool on escape, don't exit sketch

* Make scrollbar on toolbar smaller

* Add escape to exit sketch mode

* tidy up usestore

* clear scene on empty file

* disable sketch mode and re-execute on sketch loop close

* disable all but xy plane

* fix entering back into edit mode

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
2023-09-15 21:35:48 +10:00
663c396128 Add link to KCL docs to CodeMenu (#526)
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-15 00:07:11 +00:00
8db86a6783 Fix sketch mode planes visibility on enter, exit, enter (#527)
* Fix sketch mode planes visibility on enter, exit, enter

* Fix tsc

* Rename to something that makes more sense
2023-09-15 10:03:06 +10:00
d7ad7c749e Franknoirot/fix prod tauri auth (#531)
* Invoke tauri-based logout if in tauri

* Add kittycad Rust library

* Add logout and get_user Tauri commands

* Invoke get_user instead of fetching in Tauri

* @jessfraz review

Signed-off-by: Frank Noirot <frank@kittycad.io>

* Remove unnecessary logout command

* Fix rushed last commit

---------

Signed-off-by: Frank Noirot <frank@kittycad.io>
2023-09-14 19:31:16 -04:00
6e3c642d22 add expectations to modeling repo (#530) 2023-09-14 15:41:38 -07:00
4d7433ff3a Nicer geometry types (#522)
* Angle type

* Use Point2d

* use angle in more places

* Fix doctests

* Use angle in more places

* Import pi
2023-09-14 14:51:26 -07:00
4e93146559 Remove old deps (#529)
Updates chrono to remove time 0.1
Updates openapitor to remove clap 3
2023-09-14 15:05:07 -06:00
731a9bfbdb Enable devtools on Tauri builds (#525) 2023-09-14 10:25:17 -04:00
cdb4c36cf5 addNewSketchLn should close when latest point matches start (#479)
* addNewSketchLn should close when latest point matches start

* Fix types

* Include close in test case

* Add handling for continuing to sketch

* Fix types again

* close line edits (#523)

* add close to pipe

* undo some previous changes

---------

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2023-09-14 09:34:37 -04:00
45 changed files with 1693 additions and 709 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.7.0", "version": "0.7.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.9.0", "@codemirror/autocomplete": "^6.9.0",

42
public/expectations.md Normal file
View File

@ -0,0 +1,42 @@
## Alpha Users Expectations
### Welcome
First off, thank you so much for your interest in being a part of the closed Alpha program! We are thrilled to have others use our product and see what you build with it (and truthfully, how you break it too).
### KittyCAD Modeling App (KCMA)
What we are introducing to you is our KittyCAD Modeling App (KCMA). KCMA is a CAD application that expresses a hybrid style of traditional CAD interface along with a code-CAD interface. KCMA is a great way for us to test our own APIs as well as inspire others to develop their own applications.
### Why Code?
Plenty of you have professional CAD experience, and may not understand why coding your model would be helpful. The "code-CAD" paradigm isnt as popular as traditional CAD programs (SolidWorks, NX, CREO, OnShape, etc.), but it certainly has its benefits. Some benefits include:
- Automation and parametric design
- Customization and flexibility
- Algorithmic and generative design
- Reproducibility
- Easier integration with other tools
### Before You Use KCMA
Before you dive straight into the app, we wanted to lay some expectations out for you.
- KCMA is in early development. Kurt pitched the idea back in January, and the team has been working hard on it since then. KCMA has really basic CAD features for now, but we have plenty of features on our roadmap. Most of the features that you may be currently used to in your CAD workflow today will be available down the road.
- For a list of all scripting functions, please reference our [documentation](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md). For a basic rundown of our types, please reference [this document](https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/types.md).
- With that being said, we have created an external new features list in [GH Discussions](https://github.com/KittyCAD/modeling-app/discussions). For our current priority list, please click [here](https://github.com/KittyCAD/modeling-app/blob/main/public/roadmap.md). Please upvote any features in the GH Discussions page that you would like to see implemented first. We will prioritize the highest upvoted items or items that are foundational for other features on the list. You can also add your own, but we will review it to make sure its not a duplicate or its feasible for the current state of the app.
- Please report any and all bugs/issues you find. Even the smallest bugs are important! You can report them in a GH Issue [here](https://github.com/KittyCAD/modeling-app/issues/new). You are more than welcome to link your GH Issue in the **bugs** section of our Discord, but if you want to discuss the bug further, please keep that in the GH Issue thread. Please include the severity of the bug in your GH Issue ticket (High, Medium, or Low). If you are having trouble deciding what severity the bug is, use this guideline:
- **High:** The bug is blocking you from continuing.
- Example: Every time I click the extrude button with two faces selected, the app crashes.
- **Medium:** You can find a workaround to the problem, but it increases your time spent working or makes it unenjoyable.
- Example: When the app is full screen on Mac, the settings are not showing properly. It works if I have the app windowed.
- **Low:** The bug is annoying but doesnt affect workflow or block you from continuing (usually you can say “It would be nice if ___, but its not needed”)
- Example: It would be nice if the camera would orient normal to the sketching surface when I select a face/plane and click “sketch”.
- We want you all to be aware that we may reach out to you in regard to issues, bugs, problems, and satisfaction. This will typically be for further clarification so we can really nail things down.
### Discord
We will be using Discord a lot more now that the Alpha has been released to people outside of the company. Please feel free to discuss and talk with us in the **alpha users** section of the server. We highly encourage you to engage with us on Discord!
### Thank You!
Once again, from all of us to you, thank you for being a part of the closed Alpha. We are happy to chat with you all, hear your feedback, and see some of your projects!

712
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,13 +16,14 @@ tauri-build = { version = "1.4.0", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kittycad = "0.2.25"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri = { version = "1.4.1", features = ["dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater"] } tauri = { version = "1.4.1", features = ["dialog-all", "fs-all", "http-request", "path-all", "shell-open", "shell-open-api", "updater", "devtools"] }
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.32.0", features = ["time"] } tokio = { version = "1.32.0", features = ["time"] }
toml = "0.8.0" toml = "0.8.0"
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.

View File

@ -85,6 +85,24 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
Ok(token) Ok(token)
} }
///This command returns the KittyCAD user info given a token.
/// The string returned from this method is the user info as a json string.
#[tauri::command]
async fn get_user(token: Option<String>) -> Result<kittycad::types::User, InvokeError> {
println!("Getting user info...");
// use kittycad library to fetch the user info from /user/me
let client = kittycad::Client::new(token.unwrap());
let user_info: kittycad::types::User = client
.users()
.get_self()
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
Ok(user_info)
}
fn main() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.setup(|app| { .setup(|app| {
@ -97,7 +115,12 @@ fn main() {
} }
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file]) .invoke_handler(tauri::generate_handler![
get_user,
login,
read_toml,
read_txt_file
])
.plugin(tauri_plugin_fs_extra::init()) .plugin(tauri_plugin_fs_extra::init())
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "kittycad-modeling", "productName": "kittycad-modeling",
"version": "0.7.0" "version": "0.7.1"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@ -31,7 +31,7 @@ import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor' import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager' import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useCodeEval } from 'hooks/useCodeEval' import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
export function App() { export function App() {
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
@ -47,8 +47,10 @@ export function App() {
didDragInStream, didDragInStream,
streamDimensions, streamDimensions,
guiMode, guiMode,
setGuiMode,
} = useStore((s) => ({ } = useStore((s) => ({
guiMode: s.guiMode, guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
setCode: s.setCode, setCode: s.setCode,
engineCommandManager: s.engineCommandManager, engineCommandManager: s.engineCommandManager,
buttonDownInStream: s.buttonDownInStream, buttonDownInStream: s.buttonDownInStream,
@ -82,6 +84,38 @@ export function App() {
useHotkeys('shift + l', () => togglePane('logs')) useHotkeys('shift + l', () => togglePane('logs'))
useHotkeys('shift + e', () => togglePane('kclErrors')) useHotkeys('shift + e', () => togglePane('kclErrors'))
useHotkeys('shift + d', () => togglePane('debug')) useHotkeys('shift + d', () => togglePane('debug'))
useHotkeys('esc', () => {
if (guiMode.mode === 'sketch') {
if (guiMode.sketchMode === 'selectFace') return
if (guiMode.sketchMode === 'sketchEdit') {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' },
})
setGuiMode({ mode: 'default' })
} else {
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_tool',
tool: 'select',
},
})
setGuiMode({
mode: 'sketch',
sketchMode: 'sketchEdit',
rotation: guiMode.rotation,
position: guiMode.position,
pathToNode: guiMode.pathToNode,
// todo: ...guiMod is adding isTooltip: true, will probably just fix with xstate migtaion
})
}
} else {
setGuiMode({ mode: 'default' })
}
})
const paneOpacity = const paneOpacity =
onboardingStatus === onboardingPaths.CAMERA onboardingStatus === onboardingPaths.CAMERA
@ -105,7 +139,7 @@ export function App() {
}, [loadedCode, setCode]) }, [loadedCode, setCode])
useSetupEngineManager(streamRef, token) useSetupEngineManager(streamRef, token)
useCodeEval() useEngineConnectionSubscriptions()
const debounceSocketSend = throttle<EngineCommand>((message) => { const debounceSocketSend = throttle<EngineCommand>((message) => {
engineCommandManager?.sendSceneCommand(message) engineCommandManager?.sendSceneCommand(message)

View File

@ -47,6 +47,15 @@
@apply hover:bg-cool-20; @apply hover:bg-cool-20;
} }
.smallScrollbar::-webkit-scrollbar {
@apply h-0.5;
}
.smallScrollbar {
@apply overflow-x-auto;
scrollbar-width: thin;
}
:global(.dark) .popoverToggle { :global(.dark) .popoverToggle {
@apply hover:bg-cool-90; @apply hover:bg-cool-90;
} }

View File

@ -27,6 +27,7 @@ export const Toolbar = () => {
updateAst, updateAst,
programMemory, programMemory,
engineCommandManager, engineCommandManager,
executeAst,
} = useStore((s) => ({ } = useStore((s) => ({
guiMode: s.guiMode, guiMode: s.guiMode,
setGuiMode: s.setGuiMode, setGuiMode: s.setGuiMode,
@ -35,6 +36,7 @@ export const Toolbar = () => {
updateAst: s.updateAst, updateAst: s.updateAst,
programMemory: s.programMemory, programMemory: s.programMemory,
engineCommandManager: s.engineCommandManager, engineCommandManager: s.engineCommandManager,
executeAst: s.executeAst,
})) }))
useAppMode() useAppMode()
@ -44,7 +46,7 @@ export const Toolbar = () => {
function ToolbarButtons() { function ToolbarButtons() {
return ( return (
<span className="overflow-x-auto"> <span className={styles.smallScrollbar}>
{guiMode.mode === 'default' && ( {guiMode.mode === 'default' && (
<button <button
onClick={() => { onClick={() => {
@ -70,7 +72,7 @@ export const Toolbar = () => {
pathToNode, pathToNode,
programMemory programMemory
) )
updateAst(modifiedAst) updateAst(modifiedAst, true)
}} }}
> >
SketchOnFace SketchOnFace
@ -113,7 +115,7 @@ export const Toolbar = () => {
ast, ast,
pathToNode pathToNode
) )
updateAst(modifiedAst, { focusPath: pathToExtrudeArg }) updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
}} }}
> >
ExtrudeSketch ExtrudeSketch
@ -130,7 +132,7 @@ export const Toolbar = () => {
pathToNode, pathToNode,
false false
) )
updateAst(modifiedAst, { focusPath: pathToExtrudeArg }) updateAst(modifiedAst, true, { focusPath: pathToExtrudeArg })
}} }}
> >
ExtrudeSketch (w/o pipe) ExtrudeSketch (w/o pipe)
@ -146,7 +148,14 @@ export const Toolbar = () => {
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { type: 'edit_mode_exit' }, cmd: { type: 'edit_mode_exit' },
}) })
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'default_camera_disable_sketch_mode' },
})
setGuiMode({ mode: 'default' }) setGuiMode({ mode: 'default' })
executeAst()
}} }}
> >
Exit sketch Exit sketch

View File

@ -1,11 +1,15 @@
import { Menu } from '@headlessui/react' import { Menu } from '@headlessui/react'
import { PropsWithChildren } from 'react' import { PropsWithChildren } from 'react'
import { faEllipsis } from '@fortawesome/free-solid-svg-icons' import {
faArrowUpRightFromSquare,
faEllipsis,
} from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon' import { ActionIcon } from './ActionIcon'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import styles from './CodeMenu.module.css' import styles from './CodeMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor' import { editorShortcutMeta } from './TextEditor'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
export const CodeMenu = ({ children }: PropsWithChildren) => { export const CodeMenu = ({ children }: PropsWithChildren) => {
const { formatCode } = useStore((s) => ({ const { formatCode } = useStore((s) => ({
@ -52,6 +56,24 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
</button> </button>
</Menu.Item> </Menu.Item>
)} )}
<Menu.Item>
<a
className={styles.button}
href="https://github.com/KittyCAD/modeling-app/blob/main/docs/kcl/std.md"
target="_blank"
rel="noopener noreferrer"
>
<span>Read the KCL docs</span>
<small>
On GitHub
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1 align-text-top"
width={12}
/>
</small>
</a>
</Menu.Item>
</Menu.Items> </Menu.Items>
</div> </div>
</Menu> </Menu>

View File

@ -24,6 +24,9 @@ import {
StateFrom, StateFrom,
} from 'xstate' } from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { invoke } from '@tauri-apps/api'
import { isTauri } from 'lib/isTauri'
import { VITE_KC_API_BASE_URL } from 'env'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -108,6 +111,7 @@ export const GlobalStateProvider = ({
actions: { actions: {
goToSignInPage: () => { goToSignInPage: () => {
navigate(paths.SIGN_IN) navigate(paths.SIGN_IN)
logout() logout()
}, },
goToIndexPage: () => { goToIndexPage: () => {
@ -149,10 +153,12 @@ export const GlobalStateProvider = ({
export default GlobalStateProvider export default GlobalStateProvider
export function logout() { export function logout() {
const url = withBaseUrl('/logout')
localStorage.removeItem(TOKEN_PERSIST_KEY) localStorage.removeItem(TOKEN_PERSIST_KEY)
return fetch(url, { return (
method: 'POST', !isTauri() &&
credentials: 'include', fetch(withBaseUrl('/logout'), {
}) method: 'POST',
credentials: 'include',
})
)
} }

View File

@ -14,7 +14,13 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models' import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { addStartSketch } from 'lang/modifyAst' import { addStartSketch } from 'lang/modifyAst'
import { addNewSketchLn } from 'lang/std/sketch' import {
addCloseToPipe,
addNewSketchLn,
compareVec2Epsilon,
} from 'lang/std/sketch'
import { getNodeFromPath } from 'lang/queryAst'
import { Program, VariableDeclarator } from 'lang/abstractSyntaxTreeTypes'
export const Stream = ({ className = '' }) => { export const Stream = ({ className = '' }) => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -204,8 +210,8 @@ export const Stream = ({ className = '' }) => {
window: { x, y }, window: { x, y },
} }
} }
engineCommandManager?.sendSceneCommand(command).then(async ({ data }) => { engineCommandManager?.sendSceneCommand(command).then(async (resp) => {
if (command.cmd.type !== 'mouse_click' || !ast) return if (command?.cmd?.type !== 'mouse_click' || !ast) return
if ( if (
!( !(
guiMode.mode === 'sketch' && guiMode.mode === 'sketch' &&
@ -214,13 +220,28 @@ export const Stream = ({ className = '' }) => {
) )
return return
if (data?.data?.entities_modified?.length && guiMode.waitingFirstClick) { // Check if the sketch group already exists.
const varDec = getNodeFromPath<VariableDeclarator>(
ast,
guiMode.pathToNode,
'VariableDeclarator'
).node
const variableName = varDec?.id?.name
const sketchGroup = programMemory.root[variableName]
const isEditingExistingSketch =
sketchGroup?.type === 'SketchGroup' && sketchGroup.value.length
if (
resp?.data?.data?.entities_modified?.length &&
guiMode.waitingFirstClick &&
!isEditingExistingSketch
) {
const curve = await engineCommandManager?.sendSceneCommand({ const curve = await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'curve_get_control_points', type: 'curve_get_control_points',
curve_id: data?.data?.entities_modified[0], curve_id: resp?.data?.data?.entities_modified[0],
}, },
}) })
const coords: { x: number; y: number }[] = const coords: { x: number; y: number }[] =
@ -241,29 +262,65 @@ export const Stream = ({ className = '' }) => {
pathToNode: _pathToNode, pathToNode: _pathToNode,
waitingFirstClick: false, waitingFirstClick: false,
}) })
updateAst(_modifiedAst) updateAst(_modifiedAst, false)
} else if ( } else if (
data?.data?.entities_modified?.length && resp?.data?.data?.entities_modified?.length &&
!guiMode.waitingFirstClick (!guiMode.waitingFirstClick || isEditingExistingSketch)
) { ) {
const curve = await engineCommandManager?.sendSceneCommand({ const curve = await engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'curve_get_control_points', type: 'curve_get_control_points',
curve_id: data?.data?.entities_modified[0], curve_id: resp?.data?.data?.entities_modified[0],
}, },
}) })
const coords: { x: number; y: number }[] = const coords: { x: number; y: number }[] =
curve.data.data.control_points curve.data.data.control_points
const _modifiedAst = addNewSketchLn({
node: ast, const { node: varDec } = getNodeFromPath<VariableDeclarator>(
programMemory, ast,
to: [coords[1].x, coords[1].y], guiMode.pathToNode,
fnName: 'line', 'VariableDeclarator'
pathToNode: guiMode.pathToNode, )
}).modifiedAst const variableName = varDec.id.name
updateAst(_modifiedAst) const sketchGroup = programMemory.root[variableName]
if (!sketchGroup || sketchGroup.type !== 'SketchGroup') return
const initialCoords = sketchGroup.value[0].from
const isClose = compareVec2Epsilon(initialCoords, [
coords[1].x,
coords[1].y,
])
let _modifiedAst: Program
if (!isClose) {
_modifiedAst = addNewSketchLn({
node: ast,
programMemory,
to: [coords[1].x, coords[1].y],
fnName: 'line',
pathToNode: guiMode.pathToNode,
}).modifiedAst
updateAst(_modifiedAst, false)
} else {
_modifiedAst = addCloseToPipe({
node: ast,
programMemory,
pathToNode: guiMode.pathToNode,
})
setGuiMode({
mode: 'default',
})
engineCommandManager?.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sketch_mode_disable',
},
})
updateAst(_modifiedAst, true)
}
} }
}) })
setDidDragInStream(false) setDidDragInStream(false)

View File

@ -50,7 +50,7 @@ export const TextEditor = ({
const pathParams = useParams() const pathParams = useParams()
const { const {
code, code,
defferedSetCode, deferredSetCode,
editorView, editorView,
engineCommandManager, engineCommandManager,
formatCode, formatCode,
@ -60,10 +60,9 @@ export const TextEditor = ({
setEditorView, setEditorView,
setIsLSPServerReady, setIsLSPServerReady,
setSelectionRanges, setSelectionRanges,
sourceRangeMap,
} = useStore((s) => ({ } = useStore((s) => ({
code: s.code, code: s.code,
defferedSetCode: s.defferedSetCode, deferredSetCode: s.deferredSetCode,
editorView: s.editorView, editorView: s.editorView,
engineCommandManager: s.engineCommandManager, engineCommandManager: s.engineCommandManager,
formatCode: s.formatCode, formatCode: s.formatCode,
@ -73,7 +72,6 @@ export const TextEditor = ({
setEditorView: s.setEditorView, setEditorView: s.setEditorView,
setIsLSPServerReady: s.setIsLSPServerReady, setIsLSPServerReady: s.setIsLSPServerReady,
setSelectionRanges: s.setSelectionRanges, setSelectionRanges: s.setSelectionRanges,
sourceRangeMap: s.sourceRangeMap,
})) }))
const { const {
@ -126,7 +124,7 @@ export const TextEditor = ({
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => { // const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
const onChange = (value: string, viewUpdate: ViewUpdate) => { const onChange = (value: string, viewUpdate: ViewUpdate) => {
defferedSetCode(value) deferredSetCode(value)
if (isTauri() && pathParams.id) { if (isTauri() && pathParams.id) {
// Save the file to disk // Save the file to disk
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
@ -174,11 +172,11 @@ export const TextEditor = ({
) )
const idBasedSelections = codeBasedSelections const idBasedSelections = codeBasedSelections
.map(({ type, range }) => { .map(({ type, range }) => {
const hasOverlap = Object.entries(sourceRangeMap).filter( const hasOverlap = Object.entries(
([_, sourceRange]) => { engineCommandManager?.sourceRangeMap || {}
return isOverlap(sourceRange, range) ).filter(([_, sourceRange]) => {
} return isOverlap(sourceRange, range)
) })
if (hasOverlap.length) { if (hasOverlap.length) {
return { return {
type, type,

View File

@ -82,7 +82,7 @@ export const EqualAngle = () => {
transformInfos, transformInfos,
programMemory, programMemory,
}) })
updateAst(modifiedAst, { updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
}} }}

View File

@ -82,7 +82,7 @@ export const EqualLength = () => {
transformInfos, transformInfos,
programMemory, programMemory,
}) })
updateAst(modifiedAst, { updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
}} }}

View File

@ -61,7 +61,7 @@ export const HorzVert = ({
programMemory, programMemory,
referenceSegName: '', referenceSegName: '',
}) })
updateAst(modifiedAst, { updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
}} }}

View File

@ -154,7 +154,7 @@ export const Intersect = () => {
initialVariableName: 'offset', initialVariableName: 'offset',
} as any) } as any)
if (segName === tagInfo?.tag && value === valueUsedInTransform) { if (segName === tagInfo?.tag && value === valueUsedInTransform) {
updateAst(modifiedAst, { updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
} else { } else {
@ -182,7 +182,7 @@ export const Intersect = () => {
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
} }
updateAst(_modifiedAst, { updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
} }

View File

@ -65,7 +65,7 @@ export const RemoveConstrainingValues = () => {
programMemory, programMemory,
referenceSegName: '', referenceSegName: '',
}) })
updateAst(modifiedAst, { updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
}} }}

View File

@ -124,7 +124,7 @@ export const SetAbsDistance = ({
_modifiedAst.body = newBody _modifiedAst.body = newBody
} }
updateAst(_modifiedAst, { updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
} catch (e) { } catch (e) {

View File

@ -113,7 +113,7 @@ export const SetAngleBetween = () => {
initialVariableName: 'angle', initialVariableName: 'angle',
} as any) } as any)
if (segName === tagInfo?.tag && value === valueUsedInTransform) { if (segName === tagInfo?.tag && value === valueUsedInTransform) {
updateAst(modifiedAst, { updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
} else { } else {
@ -141,7 +141,7 @@ export const SetAngleBetween = () => {
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
} }
updateAst(_modifiedAst, { updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
} }

View File

@ -137,7 +137,7 @@ export const SetHorzVertDistance = ({
constraint === 'setHorzDistance' ? 'xDis' : 'yDis', constraint === 'setHorzDistance' ? 'xDis' : 'yDis',
} as any)) } as any))
if (segName === tagInfo?.tag && value === valueUsedInTransform) { if (segName === tagInfo?.tag && value === valueUsedInTransform) {
updateAst(modifiedAst, { updateAst(modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
} else { } else {
@ -163,7 +163,7 @@ export const SetHorzVertDistance = ({
) )
_modifiedAst.body = newBody _modifiedAst.body = newBody
} }
updateAst(_modifiedAst, { updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
} }

View File

@ -136,7 +136,7 @@ export const SetAngleLength = ({
_modifiedAst.body = newBody _modifiedAst.body = newBody
} }
updateAst(_modifiedAst, { updateAst(_modifiedAst, true, {
callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap), callBack: updateCursors(setCursor, selectionRanges, pathToNodeMap),
}) })
} catch (e) { } catch (e) {

View File

@ -11,8 +11,9 @@ import { isOverlap } from 'lib/utils'
interface DefaultPlanes { interface DefaultPlanes {
xy: string xy: string
yz: string // TODO re-enable
xz: string // yz: string
// xz: string
} }
export function useAppMode() { export function useAppMode() {
@ -42,34 +43,26 @@ export function useAppMode() {
y_axis: { x: 0, y: 1, z: 0 }, y_axis: { x: 0, y: 1, z: 0 },
color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 }, color: { r: 0.7, g: 0.28, b: 0.28, a: 0.4 },
}) })
const yz = createPlane(engineCommandManager, { // TODO re-enable
x_axis: { x: 0, y: 1, z: 0 }, // const yz = createPlane(engineCommandManager, {
y_axis: { x: 0, y: 0, z: 1 }, // x_axis: { x: 0, y: 1, z: 0 },
color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 }, // y_axis: { x: 0, y: 0, z: 1 },
}) // color: { r: 0.28, g: 0.7, b: 0.28, a: 0.4 },
const xz = createPlane(engineCommandManager, { // })
x_axis: { x: 1, y: 0, z: 0 }, // const xz = createPlane(engineCommandManager, {
y_axis: { x: 0, y: 0, z: 1 }, // x_axis: { x: 1, y: 0, z: 0 },
color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 }, // y_axis: { x: 0, y: 0, z: 1 },
}) // color: { r: 0.28, g: 0.28, b: 0.7, a: 0.4 },
setDefaultPlanes({ xy, yz, xz }) // })
setDefaultPlanes({ xy })
} else { } else {
hideDefaultPlanes(engineCommandManager, defaultPlanes) setDefaultPlanesHidden(engineCommandManager, defaultPlanes, false)
} }
} }
if (guiMode.mode !== 'sketch' && defaultPlanes) { if (guiMode.mode !== 'sketch' && defaultPlanes) {
Object.values(defaultPlanes).forEach((planeId) => { setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
engineCommandManager?.sendSceneCommand({ }
type: 'modeling_cmd_req', if (guiMode.mode === 'default') {
cmd_id: uuidv4(),
cmd: {
type: 'object_visible',
object_id: planeId,
hidden: true,
},
})
})
} else if (guiMode.mode === 'default') {
const pathId = const pathId =
engineCommandManager && engineCommandManager &&
isCursorInSketchCommandRange( isCursorInSketchCommandRange(
@ -128,7 +121,7 @@ export function useAppMode() {
}, },
} }
) )
hideDefaultPlanes(engineCommandManager, defaultPlanes) setDefaultPlanesHidden(engineCommandManager, defaultPlanes, true)
const sketchUuid = uuidv4() const sketchUuid = uuidv4()
const proms: any[] = [] const proms: any[] = []
proms.push( proms.push(
@ -204,9 +197,10 @@ function createPlane(
return planeId return planeId
} }
function hideDefaultPlanes( function setDefaultPlanesHidden(
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager | undefined,
defaultPlanes: DefaultPlanes defaultPlanes: DefaultPlanes,
hidden: boolean
) { ) {
Object.values(defaultPlanes).forEach((planeId) => { Object.values(defaultPlanes).forEach((planeId) => {
engineCommandManager?.sendSceneCommand({ engineCommandManager?.sendSceneCommand({
@ -215,7 +209,7 @@ function hideDefaultPlanes(
cmd: { cmd: {
type: 'object_visible', type: 'object_visible',
object_id: planeId, object_id: planeId,
hidden: true, hidden: hidden,
}, },
}) })
}) })
@ -229,15 +223,17 @@ function isCursorInSketchCommandRange(
([id, artifact]) => ([id, artifact]) =>
selectionRanges.codeBasedSelections.some( selectionRanges.codeBasedSelections.some(
(selection) => (selection) =>
Array.isArray(selection.range) && Array.isArray(selection?.range) &&
Array.isArray(artifact.range) && Array.isArray(artifact?.range) &&
isOverlap(selection.range, artifact.range) && isOverlap(selection.range, artifact.range) &&
(artifact.commandType === 'start_path' || (artifact.commandType === 'start_path' ||
artifact.commandType === 'extend_path' || artifact.commandType === 'extend_path' ||
'close_path') artifact.commandType === 'close_path')
) )
) )
return overlapingEntries.length === 1 && overlapingEntries[0][1].parentId return overlapingEntries.length && overlapingEntries[0][1].parentId
? overlapingEntries[0][1].parentId ? overlapingEntries[0][1].parentId
: false : overlapingEntries.find(
([, artifact]) => artifact.commandType === 'start_path'
)?.[0] || false
} }

View File

@ -1,156 +0,0 @@
import { useEffect } from 'react'
import { asyncParser } from '../lang/abstractSyntaxTree'
import { _executor } from '../lang/executor'
import { useStore } from '../useStore'
import { KCLError } from '../lang/errors'
// This recently moved out of app.tsx
// and is our old way of thinking that whenever the code changes we need to re-execute, instead of
// being more decisive about when and where we execute, its likey this custom hook will be
// refactored away entirely at some point
export function useCodeEval() {
const {
addLog,
addKCLError,
setAst,
setError,
setProgramMemory,
resetLogs,
resetKCLErrors,
setArtifactMap,
engineCommandManager,
highlightRange,
setHighlightRange,
setCursor2,
isStreamReady,
setIsExecuting,
defferedCode,
} = useStore((s) => ({
addLog: s.addLog,
defferedCode: s.defferedCode,
setAst: s.setAst,
setError: s.setError,
setProgramMemory: s.setProgramMemory,
resetLogs: s.resetLogs,
resetKCLErrors: s.resetKCLErrors,
setArtifactMap: s.setArtifactNSourceRangeMaps,
engineCommandManager: s.engineCommandManager,
highlightRange: s.highlightRange,
setHighlightRange: s.setHighlightRange,
setCursor2: s.setCursor2,
isStreamReady: s.isStreamReady,
addKCLError: s.addKCLError,
setIsExecuting: s.setIsExecuting,
}))
useEffect(() => {
if (!isStreamReady) return
if (!engineCommandManager) return
let unsubFn: any[] = []
const asyncWrap = async () => {
try {
if (!defferedCode) {
setAst({
start: 0,
end: 0,
body: [],
nonCodeMeta: {
noneCodeNodes: {},
start: null,
},
})
setProgramMemory({ root: {}, return: null })
engineCommandManager.endSession()
engineCommandManager.startNewSession()
return
}
const _ast = await asyncParser(defferedCode)
setAst(_ast)
resetLogs()
resetKCLErrors()
engineCommandManager.endSession()
engineCommandManager.startNewSession()
setIsExecuting(true)
const programMemory = await _executor(
_ast,
{
root: {
_0: {
type: 'UserVal',
value: 0,
__meta: [],
},
_90: {
type: 'UserVal',
value: 90,
__meta: [],
},
_180: {
type: 'UserVal',
value: 180,
__meta: [],
},
_270: {
type: 'UserVal',
value: 270,
__meta: [],
},
},
return: null,
},
engineCommandManager
)
const { artifactMap, sourceRangeMap } =
await engineCommandManager.waitForAllCommands(_ast, programMemory)
setIsExecuting(false)
if (programMemory !== undefined) {
setProgramMemory(programMemory)
}
setArtifactMap({ artifactMap, sourceRangeMap })
const unSubHover = engineCommandManager.subscribeToUnreliable({
event: 'highlight_set_entity',
callback: ({ data }) => {
if (data?.entity_id) {
const sourceRange = sourceRangeMap[data.entity_id]
setHighlightRange(sourceRange)
} else if (
!highlightRange ||
(highlightRange[0] !== 0 && highlightRange[1] !== 0)
) {
setHighlightRange([0, 0])
}
},
})
const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) {
setCursor2()
return
}
const sourceRange = sourceRangeMap[data.entity_id]
setCursor2({ range: sourceRange, type: 'default' })
},
})
unsubFn.push(unSubHover, unSubClick)
setError()
} catch (e: any) {
setIsExecuting(false)
if (e instanceof KCLError) {
addKCLError(e)
} else {
setError('problem')
console.log(e)
addLog(e)
}
}
}
asyncWrap()
return () => {
unsubFn.forEach((fn) => fn())
}
}, [defferedCode, isStreamReady, engineCommandManager])
}

View File

@ -0,0 +1,50 @@
import { useEffect } from 'react'
import { useStore } from 'useStore'
export function useEngineConnectionSubscriptions() {
const {
engineCommandManager,
setCursor2,
setHighlightRange,
highlightRange,
} = useStore((s) => ({
engineCommandManager: s.engineCommandManager,
setCursor2: s.setCursor2,
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
useEffect(() => {
if (!engineCommandManager) return
const unSubHover = engineCommandManager.subscribeToUnreliable({
event: 'highlight_set_entity',
callback: ({ data }) => {
if (data?.entity_id) {
const sourceRange =
engineCommandManager.sourceRangeMap[data.entity_id]
setHighlightRange(sourceRange)
} else if (
!highlightRange ||
(highlightRange[0] !== 0 && highlightRange[1] !== 0)
) {
setHighlightRange([0, 0])
}
},
})
const unSubClick = engineCommandManager.subscribeTo({
event: 'select_with_point',
callback: ({ data }) => {
if (!data?.entity_id) {
setCursor2()
return
}
const sourceRange = engineCommandManager.sourceRangeMap[data.entity_id]
setCursor2({ range: sourceRange, type: 'default' })
},
})
return () => {
unSubHover()
unSubClick()
}
}, [engineCommandManager, setCursor2, setHighlightRange, highlightRange])
}

View File

@ -12,11 +12,13 @@ export function useSetupEngineManager(
setMediaStream, setMediaStream,
setIsStreamReady, setIsStreamReady,
setStreamDimensions, setStreamDimensions,
executeCode,
} = useStore((s) => ({ } = useStore((s) => ({
setEngineCommandManager: s.setEngineCommandManager, setEngineCommandManager: s.setEngineCommandManager,
setMediaStream: s.setMediaStream, setMediaStream: s.setMediaStream,
setIsStreamReady: s.setIsStreamReady, setIsStreamReady: s.setIsStreamReady,
setStreamDimensions: s.setStreamDimensions, setStreamDimensions: s.setStreamDimensions,
executeCode: s.executeCode,
})) }))
const streamWidth = streamRef?.current?.offsetWidth const streamWidth = streamRef?.current?.offsetWidth
@ -41,6 +43,9 @@ export function useSetupEngineManager(
token, token,
}) })
setEngineCommandManager(eng) setEngineCommandManager(eng)
eng.waitForReady.then(() => {
executeCode()
})
return () => { return () => {
eng?.tearDown() eng?.tearDown()
} }

View File

@ -46,7 +46,7 @@ export function useConvertToVariable() {
variableName variableName
) )
updateAst(_modifiedAst) updateAst(_modifiedAst, true)
} catch (e) { } catch (e) {
console.log('e', e) console.log('e', e)
} }

View File

@ -1,4 +1,4 @@
import { Selection, TooTip } from '../useStore' import { Selection, ToolTip } from '../useStore'
import { import {
Program, Program,
CallExpression, CallExpression,
@ -305,7 +305,11 @@ export function extrudeSketch(
} }
const name = findUniqueName(node, 'part') const name = findUniqueName(node, 'part')
const VariableDeclaration = createVariableDeclaration(name, extrudeCall) const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
const showCallIndex = getShowIndex(_node) let showCallIndex = getShowIndex(_node)
if (showCallIndex == -1) {
// We didn't find a show, so let's just append everything
showCallIndex = _node.body.length
}
_node.body.splice(showCallIndex, 0, VariableDeclaration) _node.body.splice(showCallIndex, 0, VariableDeclaration)
const pathToExtrudeArg: PathToNode = [ const pathToExtrudeArg: PathToNode = [
['body', ''], ['body', ''],
@ -635,7 +639,7 @@ export function giveSketchFnCallTag(
createLiteral(tag || findUniqueName(ast, 'seg', 2))) as Literal createLiteral(tag || findUniqueName(ast, 'seg', 2))) as Literal
const tagStr = String(tagValue.value) const tagStr = String(tagValue.value)
const newFirstArg = createFirstArg( const newFirstArg = createFirstArg(
primaryCallExp.callee.name as TooTip, primaryCallExp.callee.name as ToolTip,
firstArg.val, firstArg.val,
tagValue tagValue
) )

View File

@ -1,5 +1,5 @@
import { PathToNode, ProgramMemory, SketchGroup, SourceRange } from './executor' import { PathToNode, ProgramMemory, SketchGroup, SourceRange } from './executor'
import { Selection, TooTip } from '../useStore' import { Selection, ToolTip } from '../useStore'
import { import {
BinaryExpression, BinaryExpression,
Program, Program,
@ -457,7 +457,7 @@ export function isLinesParallelAndConstrained(
const secondaryFirstArg = getFirstArg(secondaryNode) const secondaryFirstArg = getFirstArg(secondaryNode)
const constraintType = getConstraintType( const constraintType = getConstraintType(
secondaryFirstArg.val, secondaryFirstArg.val,
secondaryNode.callee.name as TooTip secondaryNode.callee.name as ToolTip
) )
const constraintLevel = getConstraintLevelFromSourceRange( const constraintLevel = getConstraintLevelFromSourceRange(
secondaryLine.range, secondaryLine.range,

View File

@ -395,8 +395,6 @@ export class EngineConnection {
let videoTrack = mediaStream.getVideoTracks()[0] let videoTrack = mediaStream.getVideoTracks()[0]
this.pc?.getStats(videoTrack).then((videoTrackStats) => { this.pc?.getStats(videoTrack).then((videoTrackStats) => {
// TODO(paultag): this needs type information from the KittyCAD typescript
// library once it's updated
let client_metrics: ClientMetrics = { let client_metrics: ClientMetrics = {
rtc_frames_decoded: 0, rtc_frames_decoded: 0,
rtc_frames_dropped: 0, rtc_frames_dropped: 0,
@ -424,12 +422,13 @@ export class EngineConnection {
videoTrackReport.framesReceived videoTrackReport.framesReceived
client_metrics.rtc_frames_per_second = client_metrics.rtc_frames_per_second =
videoTrackReport.framesPerSecond || 0 videoTrackReport.framesPerSecond || 0
client_metrics.rtc_freeze_count = videoTrackReport.freezeCount client_metrics.rtc_freeze_count =
videoTrackReport.freezeCount || 0
client_metrics.rtc_jitter_sec = videoTrackReport.jitter client_metrics.rtc_jitter_sec = videoTrackReport.jitter
client_metrics.rtc_keyframes_decoded = client_metrics.rtc_keyframes_decoded =
videoTrackReport.keyFramesDecoded videoTrackReport.keyFramesDecoded
client_metrics.rtc_total_freezes_duration_sec = client_metrics.rtc_total_freezes_duration_sec =
videoTrackReport.totalFreezesDuration videoTrackReport.totalFreezesDuration || 0
} else if (videoTrackReport.type === 'transport') { } else if (videoTrackReport.type === 'transport') {
// videoTrackReport.bytesReceived, // videoTrackReport.bytesReceived,
// videoTrackReport.bytesSent, // videoTrackReport.bytesSent,
@ -908,14 +907,14 @@ export class EngineCommandManager {
} }
private async fixIdMappings(ast: Program, programMemory: ProgramMemory) { private async fixIdMappings(ast: Program, programMemory: ProgramMemory) {
/* This is a temporary solution since the cmd_ids that are sent through when /* This is a temporary solution since the cmd_ids that are sent through when
sending 'extend_path' ids are not used as the segment ids. sending 'extend_path' ids are not used as the segment ids.
We have a way to back fill them with 'path_get_info', however this relies on one We have a way to back fill them with 'path_get_info', however this relies on one
the sketchGroup array and the segements array returned from the server to be in the sketchGroup array and the segements array returned from the server to be in
the same length and order. plus it's super hacky, we first use the path_id to get the same length and order. plus it's super hacky, we first use the path_id to get
the source range of the pipe expression then use the name of the variable to get the source range of the pipe expression then use the name of the variable to get
the sketchGroup from programMemory. the sketchGroup from programMemory.
I feel queezy about relying on all these steps to always line up. I feel queezy about relying on all these steps to always line up.
We have also had to pollute this EngineCommandManager class with knowledge of both the ast and programMemory We have also had to pollute this EngineCommandManager class with knowledge of both the ast and programMemory
We should get the cmd_ids to match with the segment ids and delete this method. We should get the cmd_ids to match with the segment ids and delete this method.

View File

@ -4,6 +4,7 @@ import {
addNewSketchLn, addNewSketchLn,
getYComponent, getYComponent,
getXComponent, getXComponent,
addCloseToPipe,
} from './sketch' } from './sketch'
import { parser_wasm } from '../abstractSyntaxTree' import { parser_wasm } from '../abstractSyntaxTree'
import { getNodePathFromSourceRange } from '../queryAst' import { getNodePathFromSourceRange } from '../queryAst'
@ -146,7 +147,7 @@ show(mySketch001)`
const programMemory = await enginelessExecutor(ast) const programMemory = await enginelessExecutor(ast)
const sourceStart = code.indexOf(lineToChange) const sourceStart = code.indexOf(lineToChange)
expect(sourceStart).toBe(66) expect(sourceStart).toBe(66)
const { modifiedAst } = addNewSketchLn({ let { modifiedAst } = addNewSketchLn({
node: ast, node: ast,
programMemory, programMemory,
to: [2, 3], to: [2, 3],
@ -160,12 +161,33 @@ show(mySketch001)`
], ],
}) })
// Enable rotations #152 // Enable rotations #152
const expectedCode = `const mySketch001 = startSketchAt([0, 0]) let expectedCode = `const mySketch001 = startSketchAt([0, 0])
// |> rx(45, %) // |> rx(45, %)
|> lineTo([-1.59, -1.54], %) |> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %) |> lineTo([0.46, -5.82], %)
|> lineTo([2, 3], %) |> lineTo([2, 3], %)
show(mySketch001) show(mySketch001)
`
expect(recast(modifiedAst)).toBe(expectedCode)
modifiedAst = addCloseToPipe({
node: ast,
programMemory,
pathToNode: [
['body', ''],
[0, 'index'],
['declarations', 'VariableDeclaration'],
[0, 'index'],
['init', 'VariableDeclarator'],
],
})
expectedCode = `const mySketch001 = startSketchAt([0, 0])
// |> rx(45, %)
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> close(%)
show(mySketch001)
` `
expect(recast(modifiedAst)).toBe(expectedCode) expect(recast(modifiedAst)).toBe(expectedCode)
}) })

View File

@ -21,7 +21,7 @@ import {
getNodePathFromSourceRange, getNodePathFromSourceRange,
} from '../queryAst' } from '../queryAst'
import { isLiteralArrayOrStatic } from './sketchcombos' import { isLiteralArrayOrStatic } from './sketchcombos'
import { GuiModes, toolTips, TooTip } from '../../useStore' import { GuiModes, toolTips, ToolTip } from '../../useStore'
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst' import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
import { generateUuidFromHashSeed } from '../../lib/uuid' import { generateUuidFromHashSeed } from '../../lib/uuid'
@ -57,7 +57,7 @@ export function getCoordsFromPaths(skGroup: SketchGroup, index = 0): Coords2d {
} }
export function createFirstArg( export function createFirstArg(
sketchFn: TooTip, sketchFn: ToolTip,
val: Value | [Value, Value] | [Value, Value, Value], val: Value | [Value, Value] | [Value, Value, Value],
tag?: Value tag?: Value
): Value { ): Value {
@ -943,17 +943,29 @@ interface CreateLineFnCallArgs {
programMemory: ProgramMemory programMemory: ProgramMemory
to: [number, number] to: [number, number]
from: [number, number] from: [number, number]
fnName: TooTip fnName: ToolTip
pathToNode: PathToNode pathToNode: PathToNode
} }
export function compareVec2Epsilon(
vec1: [number, number],
vec2: [number, number]
) {
const compareEpsilon = 0.015625 // or 2^-6
const xDifference = Math.abs(vec1[0] - vec2[0])
const yDifference = Math.abs(vec1[0] - vec2[0])
return xDifference < compareEpsilon && yDifference < compareEpsilon
}
export function addNewSketchLn({ export function addNewSketchLn({
node: _node, node: _node,
programMemory: previousProgramMemory, programMemory: previousProgramMemory,
to, to,
fnName, fnName,
pathToNode, pathToNode,
}: Omit<CreateLineFnCallArgs, 'from'>): { modifiedAst: Program } { }: Omit<CreateLineFnCallArgs, 'from'>): {
modifiedAst: Program
} {
const node = JSON.parse(JSON.stringify(_node)) const node = JSON.parse(JSON.stringify(_node))
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {} const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
if (!add || !updateArgs) throw new Error('not a sketch line helper') if (!add || !updateArgs) throw new Error('not a sketch line helper')
@ -971,7 +983,6 @@ export function addNewSketchLn({
const last = sketch.value[sketch.value.length - 1] || sketch.start const last = sketch.value[sketch.value.length - 1] || sketch.start
const from = last.to const from = last.to
return add({ return add({
node, node,
previousProgramMemory, previousProgramMemory,
@ -982,6 +993,29 @@ export function addNewSketchLn({
}) })
} }
export function addCloseToPipe({
node,
pathToNode,
}: {
node: Program
programMemory: ProgramMemory
pathToNode: PathToNode
}) {
const _node = { ...node }
const closeExpression = createCallExpression('close', [
createPipeSubstitution(),
])
const pipeExpression = getNodeFromPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
).node
if (pipeExpression.type !== 'PipeExpression')
throw new Error('not a pipe expression')
pipeExpression.body = [...pipeExpression.body, closeExpression]
return _node
}
export function replaceSketchLine({ export function replaceSketchLine({
node, node,
programMemory, programMemory,
@ -995,7 +1029,7 @@ export function replaceSketchLine({
node: Program node: Program
programMemory: ProgramMemory programMemory: ProgramMemory
sourceRange: SourceRange sourceRange: SourceRange
fnName: TooTip fnName: ToolTip
to: [number, number] to: [number, number]
from: [number, number] from: [number, number]
createCallback: TransformCallback createCallback: TransformCallback
@ -1174,7 +1208,7 @@ function getFirstArgValuesForAngleFns(callExpression: CallExpression): {
const tag = firstArg.properties.find((p) => p.key.name === 'tag')?.value const tag = firstArg.properties.find((p) => p.key.name === 'tag')?.value
const angle = firstArg.properties.find((p) => p.key.name === 'angle')?.value const angle = firstArg.properties.find((p) => p.key.name === 'angle')?.value
const secondArgName = ['angledLineToX', 'angledLineToY'].includes( const secondArgName = ['angledLineToX', 'angledLineToY'].includes(
callExpression?.callee?.name as TooTip callExpression?.callee?.name as ToolTip
) )
? 'to' ? 'to'
: 'length' : 'length'

View File

@ -1,4 +1,4 @@
import { TooTip, toolTips } from '../../useStore' import { ToolTip, toolTips } from '../../useStore'
import { import {
Program, Program,
VariableDeclarator, VariableDeclarator,
@ -67,7 +67,10 @@ export function isSketchVariablesLinked(
return false return false
const firstCallExp = // first in pipe expression or just the call expression const firstCallExp = // first in pipe expression or just the call expression
init?.type === 'CallExpression' ? init : (init?.body[0] as CallExpression) init?.type === 'CallExpression' ? init : (init?.body[0] as CallExpression)
if (!firstCallExp || !toolTips.includes(firstCallExp?.callee?.name as TooTip)) if (
!firstCallExp ||
!toolTips.includes(firstCallExp?.callee?.name as ToolTip)
)
return false return false
// convention for sketch fns is that the second argument is the sketch group // convention for sketch fns is that the second argument is the sketch group
const secondArg = firstCallExp?.arguments[1] const secondArg = firstCallExp?.arguments[1]

View File

@ -9,7 +9,7 @@ import {
getConstraintLevelFromSourceRange, getConstraintLevelFromSourceRange,
} from './sketchcombos' } from './sketchcombos'
import { initPromise } from '../rust' import { initPromise } from '../rust'
import { Selections, TooTip } from '../../useStore' import { Selections, ToolTip } from '../../useStore'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
import { recast } from '../../lang/recast' import { recast } from '../../lang/recast'
@ -68,7 +68,7 @@ function getConstraintTypeFromSourceHelper(
Value, Value,
Value Value
] ]
const fnName = (ast.body[0] as any).expression.callee.name as TooTip const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
return getConstraintType(args, fnName) return getConstraintType(args, fnName)
} }
function getConstraintTypeFromSourceHelper2( function getConstraintTypeFromSourceHelper2(
@ -76,7 +76,7 @@ function getConstraintTypeFromSourceHelper2(
): ReturnType<typeof getConstraintType> { ): ReturnType<typeof getConstraintType> {
const ast = parser_wasm(code) const ast = parser_wasm(code)
const arg = (ast.body[0] as any).expression.arguments[0] as Value const arg = (ast.body[0] as any).expression.arguments[0] as Value
const fnName = (ast.body[0] as any).expression.callee.name as TooTip const fnName = (ast.body[0] as any).expression.callee.name as ToolTip
return getConstraintType(arg, fnName) return getConstraintType(arg, fnName)
} }

View File

@ -1,5 +1,5 @@
import { TransformCallback } from './stdTypes' import { TransformCallback } from './stdTypes'
import { Selections, toolTips, TooTip, Selection } from '../../useStore' import { Selections, toolTips, ToolTip, Selection } from '../../useStore'
import { import {
CallExpression, CallExpression,
Program, Program,
@ -54,7 +54,7 @@ export type ConstraintType =
| 'setAngleBetween' | 'setAngleBetween'
function createCallWrapper( function createCallWrapper(
a: TooTip, a: ToolTip,
val: [Value, Value] | Value, val: [Value, Value] | Value,
tag?: Value, tag?: Value,
valueUsedInTransform?: number valueUsedInTransform?: number
@ -101,7 +101,7 @@ function intersectCallWrapper({
} }
export type TransformInfo = { export type TransformInfo = {
tooltip: TooTip tooltip: ToolTip
createNode: (a: { createNode: (a: {
varValA: Value // x / angle varValA: Value // x / angle
varValB: Value // y / length or x y for angledLineOfXlength etc varValB: Value // y / length or x y for angledLineOfXlength etc
@ -112,7 +112,7 @@ export type TransformInfo = {
} }
type TransformMap = { type TransformMap = {
[key in TooTip]?: { [key in ToolTip]?: {
[key in LineInputsType | 'free']?: { [key in LineInputsType | 'free']?: {
[key in ConstraintType]?: TransformInfo [key in ConstraintType]?: TransformInfo
} }
@ -1095,12 +1095,12 @@ export function getRemoveConstraintsTransform(
sketchFnExp: CallExpression, sketchFnExp: CallExpression,
constraintType: ConstraintType constraintType: ConstraintType
): TransformInfo | false { ): TransformInfo | false {
let name = sketchFnExp.callee.name as TooTip let name = sketchFnExp.callee.name as ToolTip
if (!toolTips.includes(name)) { if (!toolTips.includes(name)) {
return false return false
} }
const xyLineMap: { const xyLineMap: {
[key in TooTip]?: TooTip [key in ToolTip]?: ToolTip
} = { } = {
xLine: 'line', xLine: 'line',
yLine: 'line', yLine: 'line',
@ -1167,12 +1167,12 @@ function getTransformMapPath(
constraintType: ConstraintType constraintType: ConstraintType
): ):
| { | {
toolTip: TooTip toolTip: ToolTip
lineInputType: LineInputsType | 'free' lineInputType: LineInputsType | 'free'
constraintType: ConstraintType constraintType: ConstraintType
} }
| false { | false {
const name = sketchFnExp.callee.name as TooTip const name = sketchFnExp.callee.name as ToolTip
if (!toolTips.includes(name)) { if (!toolTips.includes(name)) {
return false return false
} }
@ -1225,7 +1225,7 @@ export function getTransformInfo(
export function getConstraintType( export function getConstraintType(
val: Value | [Value, Value] | [Value, Value, Value], val: Value | [Value, Value] | [Value, Value, Value],
fnName: TooTip fnName: ToolTip
): LineInputsType | null { ): LineInputsType | null {
// this function assumes that for two val sketch functions that one arg is locked down not both // this function assumes that for two val sketch functions that one arg is locked down not both
// and for one val sketch functions that the arg is NOT locked down // and for one val sketch functions that the arg is NOT locked down
@ -1445,7 +1445,7 @@ export function transformAstSketchLines({
programMemory, programMemory,
sourceRange: range, sourceRange: range,
referencedSegment, referencedSegment,
fnName: transformTo || (callExp.callee.name as TooTip), fnName: transformTo || (callExp.callee.name as ToolTip),
to, to,
from, from,
createCallback: callBack({ createCallback: callBack({
@ -1511,7 +1511,7 @@ export function getConstraintLevelFromSourceRange(
getNodePathFromSourceRange(ast, cursorRange), getNodePathFromSourceRange(ast, cursorRange),
'CallExpression' 'CallExpression'
) )
const name = sketchFnExp?.callee?.name as TooTip const name = sketchFnExp?.callee?.name as ToolTip
if (!toolTips.includes(name)) return 'free' if (!toolTips.includes(name)) return 'free'
const firstArg = getFirstArg(sketchFnExp) const firstArg = getFirstArg(sketchFnExp)

View File

@ -1,6 +1,6 @@
import { ProgramMemory, Path, SourceRange } from '../executor' import { ProgramMemory, Path, SourceRange } from '../executor'
import { Program, Value } from '../abstractSyntaxTreeTypes' import { Program, Value } from '../abstractSyntaxTreeTypes'
import { TooTip } from '../../useStore' import { ToolTip } from '../../useStore'
import { PathToNode } from '../executor' import { PathToNode } from '../executor'
import { EngineCommandManager } from './engineConnection' import { EngineCommandManager } from './engineConnection'
@ -45,7 +45,7 @@ export type TransformCallback = (
} }
export type SketchCallTransfromMap = { export type SketchCallTransfromMap = {
[key in TooTip]: TransformCallback [key in ToolTip]: TransformCallback
} }
export interface SketchLineHelper { export interface SketchLineHelper {

View File

@ -57,7 +57,7 @@ export function throttle<T>(
} }
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset // takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
export function defferExecution<T>(func: (args: T) => any, wait: number) { export function deferExecution<T>(func: (args: T) => any, wait: number) {
let timeout: ReturnType<typeof setTimeout> | null let timeout: ReturnType<typeof setTimeout> | null
let latestArgs: T let latestArgs: T
@ -66,7 +66,7 @@ export function defferExecution<T>(func: (args: T) => any, wait: number) {
func(latestArgs) func(latestArgs)
} }
function deffered(args: T) { function deferred(args: T) {
latestArgs = args latestArgs = args
if (timeout) { if (timeout) {
clearTimeout(timeout) clearTimeout(timeout)
@ -74,7 +74,7 @@ export function defferExecution<T>(func: (args: T) => any, wait: number) {
timeout = setTimeout(later, wait) timeout = setTimeout(later, wait)
} }
return deffered return deferred
} }
export function getNormalisedCoordinates({ export function getNormalisedCoordinates({

View File

@ -2,6 +2,8 @@ import { createMachine, assign } from 'xstate'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import withBaseURL from '../lib/withBaseURL' import withBaseURL from '../lib/withBaseURL'
import { CommandBarMeta } from '../lib/commands' import { CommandBarMeta } from '../lib/commands'
import { isTauri } from 'lib/isTauri'
import { invoke } from '@tauri-apps/api'
const SKIP_AUTH = const SKIP_AUTH =
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
@ -115,16 +117,25 @@ async function getUser(context: UserContext) {
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
if (!context.token && '__TAURI__' in window) throw 'not log in'
if (!context.token && isTauri()) throw new Error('No token found')
if (context.token) headers['Authorization'] = `Bearer ${context.token}` if (context.token) headers['Authorization'] = `Bearer ${context.token}`
if (SKIP_AUTH) return LOCAL_USER if (SKIP_AUTH) return LOCAL_USER
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
const user = await response.json() const userPromise = !isTauri()
? fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
.then((res) => res.json())
.catch((err) => console.error('error from Browser getUser', err))
: invoke<Models['User_type'] | Record<'error_code', unknown>>('get_user', {
token: context.token,
}).catch((err) => console.error('error from Tauri getUser', err))
const user = await userPromise
if ('error_code' in user) throw new Error(user.message) if ('error_code' in user) throw new Error(user.message)
return user return user

View File

@ -4,6 +4,7 @@ import { addLineHighlight, EditorView } from './editor/highlightextension'
import { parser_wasm } from './lang/abstractSyntaxTree' import { parser_wasm } from './lang/abstractSyntaxTree'
import { Program } from './lang/abstractSyntaxTreeTypes' import { Program } from './lang/abstractSyntaxTreeTypes'
import { getNodeFromPath } from './lang/queryAst' import { getNodeFromPath } from './lang/queryAst'
import { enginelessExecutor } from './lib/testHelpers'
import { import {
ProgramMemory, ProgramMemory,
Position, Position,
@ -13,13 +14,10 @@ import {
} from './lang/executor' } from './lang/executor'
import { recast } from './lang/recast' import { recast } from './lang/recast'
import { EditorSelection } from '@codemirror/state' import { EditorSelection } from '@codemirror/state'
import { import { EngineCommandManager } from './lang/std/engineConnection'
ArtifactMap,
SourceRangeMap,
EngineCommandManager,
} from './lang/std/engineConnection'
import { KCLError } from './lang/errors' import { KCLError } from './lang/errors'
import { defferExecution } from 'lib/utils' import { deferExecution } from 'lib/utils'
import { _executor } from './lang/executor'
export type Selection = { export type Selection = {
type: 'default' | 'line-end' | 'line-mid' type: 'default' | 'line-end' | 'line-mid'
@ -29,7 +27,7 @@ export type Selections = {
otherSelections: ('y-axis' | 'x-axis' | 'z-axis')[] otherSelections: ('y-axis' | 'x-axis' | 'z-axis')[]
codeBasedSelections: Selection[] codeBasedSelections: Selection[]
} }
export type TooTip = export type ToolTip =
| 'lineTo' | 'lineTo'
| 'line' | 'line'
| 'angledLine' | 'angledLine'
@ -59,7 +57,7 @@ export const toolTips = [
'xLineTo', 'xLineTo',
'yLineTo', 'yLineTo',
'angledLineThatIntersects', 'angledLineThatIntersects',
] as any as TooTip[] ] as any as ToolTip[]
export type GuiModes = export type GuiModes =
| { | {
@ -67,7 +65,7 @@ export type GuiModes =
} }
| { | {
mode: 'sketch' mode: 'sketch'
sketchMode: TooTip sketchMode: ToolTip
isTooltip: true isTooltip: true
waitingFirstClick: boolean waitingFirstClick: boolean
rotation: Rotation rotation: Rotation
@ -123,40 +121,37 @@ export interface StoreState {
setGuiMode: (guiMode: GuiModes) => void setGuiMode: (guiMode: GuiModes) => void
logs: string[] logs: string[]
addLog: (log: string) => void addLog: (log: string) => void
resetLogs: () => void setLogs: (logs: string[]) => void
kclErrors: KCLError[] kclErrors: KCLError[]
addKCLError: (err: KCLError) => void addKCLError: (err: KCLError) => void
setErrors: (errors: KCLError[]) => void
resetKCLErrors: () => void resetKCLErrors: () => void
ast: Program ast: Program
setAst: (ast: Program) => void setAst: (ast: Program) => void
executeAst: (ast?: Program) => void
executeAstMock: (ast?: Program) => void
updateAst: ( updateAst: (
ast: Program, ast: Program,
execute: boolean,
optionalParams?: { optionalParams?: {
focusPath?: PathToNode focusPath?: PathToNode
callBack?: (ast: Program) => void callBack?: (ast: Program) => void
} }
) => void ) => void
updateAstAsync: (ast: Program, focusPath?: PathToNode) => void updateAstAsync: (
ast: Program,
reexecute: boolean,
focusPath?: PathToNode
) => void
code: string code: string
defferedCode: string
setCode: (code: string) => void setCode: (code: string) => void
defferedSetCode: (code: string) => void deferredSetCode: (code: string) => void
executeCode: (code?: string) => void
formatCode: () => void formatCode: () => void
errorState: {
isError: boolean
error: string
}
setError: (error?: string) => void
programMemory: ProgramMemory programMemory: ProgramMemory
setProgramMemory: (programMemory: ProgramMemory) => void setProgramMemory: (programMemory: ProgramMemory) => void
isShiftDown: boolean isShiftDown: boolean
setIsShiftDown: (isShiftDown: boolean) => void setIsShiftDown: (isShiftDown: boolean) => void
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
setArtifactNSourceRangeMaps: (a: {
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
}) => void
engineCommandManager?: EngineCommandManager engineCommandManager?: EngineCommandManager
setEngineCommandManager: (engineCommandManager: EngineCommandManager) => void setEngineCommandManager: (engineCommandManager: EngineCommandManager) => void
mediaStream?: MediaStream mediaStream?: MediaStream
@ -197,10 +192,13 @@ let pendingAstUpdates: number[] = []
export const useStore = create<StoreState>()( export const useStore = create<StoreState>()(
persist( persist(
(set, get) => { (set, get) => {
const setDefferedCode = defferExecution( // We defer this so that likely our ast has caught up to the code.
(code: string) => set({ defferedCode: code }), // If we are making changes that are not reflected in the ast, we
600 // should not be updating the ast.
) const setDeferredCode = deferExecution((code: string) => {
set({ code })
get().executeCode(code)
}, 600)
return { return {
editorView: null, editorView: null,
setEditorView: (editorView) => { setEditorView: (editorView) => {
@ -214,6 +212,22 @@ export const useStore = create<StoreState>()(
editorView.dispatch({ effects: addLineHighlight.of(selection) }) editorView.dispatch({ effects: addLineHighlight.of(selection) })
} }
}, },
executeCode: async (code) => {
const result = await executeCode({
code: code || get().code,
lastAst: get().ast,
engineCommandManager: get().engineCommandManager,
})
if (!result.isChange) {
return
}
set({
ast: result.ast,
logs: result.logs,
kclErrors: result.errors,
programMemory: result.programMemory,
})
},
setCursor: (selections) => { setCursor: (selections) => {
const { editorView } = get() const { editorView } = get()
if (!editorView) return if (!editorView) return
@ -243,7 +257,10 @@ export const useStore = create<StoreState>()(
get().setCursor({ get().setCursor({
otherSelections: currestSelections.otherSelections, otherSelections: currestSelections.otherSelections,
codeBasedSelections: [ codeBasedSelections: [
{ range: [0, code.length - 1], type: 'default' }, {
range: [0, code.length ? code.length - 1 : 0],
type: 'default',
},
], ],
}) })
return return
@ -277,8 +294,8 @@ export const useStore = create<StoreState>()(
set((state) => ({ logs: [...state.logs, log] })) set((state) => ({ logs: [...state.logs, log] }))
} }
}, },
resetLogs: () => { setLogs: (logs) => {
set({ logs: [] }) set({ logs })
}, },
kclErrors: [], kclErrors: [],
addKCLError: (e) => { addKCLError: (e) => {
@ -287,6 +304,9 @@ export const useStore = create<StoreState>()(
resetKCLErrors: () => { resetKCLErrors: () => {
set({ kclErrors: [] }) set({ kclErrors: [] })
}, },
setErrors: (errors) => {
set({ kclErrors: errors })
},
ast: { ast: {
start: 0, start: 0,
end: 0, end: 0,
@ -299,7 +319,47 @@ export const useStore = create<StoreState>()(
setAst: (ast) => { setAst: (ast) => {
set({ ast }) set({ ast })
}, },
updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => { executeAst: async (ast) => {
const _ast = ast || get().ast
if (!get().isStreamReady) return
const engineCommandManager = get().engineCommandManager!
if (!engineCommandManager) return
set({ isExecuting: true })
const { logs, errors, programMemory } = await executeAst({
ast: _ast,
engineCommandManager,
})
set({
programMemory,
logs,
kclErrors: errors,
isExecuting: false,
})
},
executeAstMock: async (ast) => {
const _ast = ast || get().ast
if (!get().isStreamReady) return
const engineCommandManager = get().engineCommandManager!
if (!engineCommandManager) return
const { logs, errors, programMemory } = await executeAst({
ast: _ast,
engineCommandManager,
useFakeExecutor: true,
})
set({
programMemory,
logs,
kclErrors: errors,
isExecuting: false,
})
},
updateAst: async (
ast,
reexecute,
{ focusPath, callBack = () => {} } = {}
) => {
const newCode = recast(ast) const newCode = recast(ast)
const astWithUpdatedSource = parser_wasm(newCode) const astWithUpdatedSource = parser_wasm(newCode)
callBack(astWithUpdatedSource) callBack(astWithUpdatedSource)
@ -307,7 +367,6 @@ export const useStore = create<StoreState>()(
set({ set({
ast: astWithUpdatedSource, ast: astWithUpdatedSource,
code: newCode, code: newCode,
defferedCode: newCode,
}) })
if (focusPath) { if (focusPath) {
const { node } = getNodeFromPath<any>( const { node } = getNodeFromPath<any>(
@ -328,24 +387,33 @@ export const useStore = create<StoreState>()(
}) })
}) })
} }
if (reexecute) {
// Call execute on the set ast.
get().executeAst(astWithUpdatedSource)
} else {
// When we don't re-execute, we still want to update the program
// memory with the new ast. So we will hit the mock executor
// instead.
get().executeAstMock(astWithUpdatedSource)
}
}, },
updateAstAsync: async (ast, focusPath) => { updateAstAsync: async (ast, reexecute, focusPath) => {
// clear any pending updates // clear any pending updates
pendingAstUpdates.forEach((id) => clearTimeout(id)) pendingAstUpdates.forEach((id) => clearTimeout(id))
pendingAstUpdates = [] pendingAstUpdates = []
// setup a new update // setup a new update
pendingAstUpdates.push( pendingAstUpdates.push(
setTimeout(() => { setTimeout(() => {
get().updateAst(ast, { focusPath }) get().updateAst(ast, reexecute, { focusPath })
}, 100) as unknown as number }, 100) as unknown as number
) )
}, },
code: '', code: '',
defferedCode: '', setCode: (code) => set({ code }),
setCode: (code) => set({ code, defferedCode: code }), deferredSetCode: (code) => {
defferedSetCode: (code) => {
set({ code }) set({ code })
setDefferedCode(code) setDeferredCode(code)
}, },
formatCode: async () => { formatCode: async () => {
const code = get().code const code = get().code
@ -353,20 +421,10 @@ export const useStore = create<StoreState>()(
const newCode = recast(ast) const newCode = recast(ast)
set({ code: newCode, ast }) set({ code: newCode, ast })
}, },
errorState: {
isError: false,
error: '',
},
setError: (error = '') => {
set({ errorState: { isError: !!error, error } })
},
programMemory: { root: {}, return: null }, programMemory: { root: {}, return: null },
setProgramMemory: (programMemory) => set({ programMemory }), setProgramMemory: (programMemory) => set({ programMemory }),
isShiftDown: false, isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }), setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
artifactMap: {},
sourceRangeMap: {},
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
setEngineCommandManager: (engineCommandManager) => setEngineCommandManager: (engineCommandManager) =>
set({ engineCommandManager }), set({ engineCommandManager }),
setMediaStream: (mediaStream) => set({ mediaStream }), setMediaStream: (mediaStream) => set({ mediaStream }),
@ -409,9 +467,165 @@ export const useStore = create<StoreState>()(
partialize: (state) => partialize: (state) =>
Object.fromEntries( Object.fromEntries(
Object.entries(state).filter(([key]) => Object.entries(state).filter(([key]) =>
['code', 'defferedCode', 'openPanes'].includes(key) ['code', 'openPanes'].includes(key)
) )
), ),
} }
) )
) )
const defaultProgramMemory: ProgramMemory['root'] = {
_0: {
type: 'UserVal',
value: 0,
__meta: [],
},
_90: {
type: 'UserVal',
value: 90,
__meta: [],
},
_180: {
type: 'UserVal',
value: 180,
__meta: [],
},
_270: {
type: 'UserVal',
value: 270,
__meta: [],
},
PI: {
type: 'UserVal',
value: Math.PI,
__meta: [],
},
}
async function executeCode({
engineCommandManager,
code,
lastAst,
}: {
code: string
lastAst: Program
engineCommandManager?: EngineCommandManager
}): Promise<
| {
logs: string[]
errors: KCLError[]
programMemory: ProgramMemory
ast: Program
isChange: true
}
| { isChange: false }
> {
let ast: Program
try {
ast = parser_wasm(code)
} catch (e) {
let errors: KCLError[] = []
let logs: string[] = [JSON.stringify(e)]
if (e instanceof KCLError) {
errors = [e]
logs = []
if (e.msg === 'file is empty') engineCommandManager?.endSession()
}
return {
isChange: true,
logs,
errors,
programMemory: {
root: {},
return: null,
},
ast: {
start: 0,
end: 0,
body: [],
nonCodeMeta: {
noneCodeNodes: {},
start: null,
},
},
}
}
// Check if the ast we have is equal to the ast in the storage.
// If it is, we don't need to update the ast.
if (!engineCommandManager || JSON.stringify(ast) === JSON.stringify(lastAst))
return { isChange: false }
const { logs, errors, programMemory } = await executeAst({
ast,
engineCommandManager,
})
return {
ast,
logs,
errors,
programMemory,
isChange: true,
}
}
async function executeAst({
ast,
engineCommandManager,
useFakeExecutor = false,
}: {
ast: Program
engineCommandManager: EngineCommandManager
useFakeExecutor?: boolean
}): Promise<{
logs: string[]
errors: KCLError[]
programMemory: ProgramMemory
}> {
try {
if (!useFakeExecutor) {
engineCommandManager.endSession()
engineCommandManager.startNewSession()
}
const programMemory = await (useFakeExecutor
? enginelessExecutor(ast, {
root: defaultProgramMemory,
return: null,
})
: _executor(
ast,
{
root: defaultProgramMemory,
return: null,
},
engineCommandManager
))
await engineCommandManager.waitForAllCommands(ast, programMemory)
return {
logs: [],
errors: [],
programMemory,
}
} catch (e: any) {
if (e instanceof KCLError) {
return {
errors: [e],
logs: [],
programMemory: {
root: {},
return: null,
},
}
} else {
console.log(e)
return {
logs: [e],
errors: [],
programMemory: {
root: {},
return: null,
},
}
}
}
}

180
src/wasm-lib/Cargo.lock generated
View File

@ -306,7 +306,7 @@ dependencies = [
"serde", "serde",
"serde_bytes", "serde_bytes",
"serde_json", "serde_json",
"time 0.3.27", "time",
"uuid", "uuid",
] ]
@ -363,18 +363,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.26" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"time 0.1.45", "windows-targets 0.48.5",
"wasm-bindgen",
"winapi",
] ]
[[package]] [[package]]
@ -388,24 +385,6 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "clap"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"atty",
"bitflags 1.3.2",
"clap_derive 3.2.25",
"clap_lex 0.2.4",
"indexmap 1.9.3",
"once_cell",
"strsim",
"termcolor",
"textwrap",
"unicase",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.4.3" version = "4.4.3"
@ -413,7 +392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive 4.4.2", "clap_derive",
] ]
[[package]] [[package]]
@ -424,25 +403,13 @@ checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
"clap_lex 0.5.1", "clap_lex",
"strsim", "strsim",
"terminal_size",
"unicase", "unicase",
"unicode-width", "unicode-width",
] ]
[[package]]
name = "clap_derive"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.4.2" version = "4.4.2"
@ -455,15 +422,6 @@ dependencies = [
"syn 2.0.33", "syn 2.0.33",
] ]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.5.1" version = "0.5.1"
@ -985,7 +943,7 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1244,7 +1202,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown 0.12.3", "hashbrown 0.12.3",
"serde",
] ]
[[package]] [[package]]
@ -1255,6 +1212,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.0", "hashbrown 0.14.0",
"serde",
] ]
[[package]] [[package]]
@ -1269,6 +1227,17 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi 0.3.2",
"libc",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.8.0" version = "2.8.0"
@ -1282,7 +1251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [ dependencies = [
"hermit-abi 0.3.2", "hermit-abi 0.3.2",
"rustix", "rustix 0.38.9",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -1345,7 +1314,7 @@ version = "0.1.26"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bson", "bson",
"clap 4.4.3", "clap",
"dashmap", "dashmap",
"derive-docs 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "derive-docs 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"expectorate", "expectorate",
@ -1393,7 +1362,7 @@ dependencies = [
"rand", "rand",
"reqwest", "reqwest",
"reqwest-conditional-middleware", "reqwest-conditional-middleware",
"reqwest-middleware 0.2.3", "reqwest-middleware",
"reqwest-retry", "reqwest-retry",
"reqwest-tracing", "reqwest-tracing",
"schemars", "schemars",
@ -1447,6 +1416,12 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.5" version = "0.4.5"
@ -1554,7 +1529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [ dependencies = [
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -1675,22 +1650,22 @@ checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
[[package]] [[package]]
name = "openapitor" name = "openapitor"
version = "0.0.5" version = "0.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/KittyCAD/kittycad.rs?branch=main#3d74c1dfb41146a268a644e9fde2c19a8cd66895"
checksum = "120168eae5b6485690af708bd1030547df62cca10a643763d416ab0e6831decb"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
"chrono", "chrono",
"clap 3.2.25", "clap",
"data-encoding", "data-encoding",
"format_serde_error", "format_serde_error",
"futures-util", "futures-util",
"http", "http",
"indexmap 1.9.3", "indexmap 2.0.0",
"json-patch", "json-patch",
"log", "log",
"numeral", "numeral",
"once_cell",
"openapiv3", "openapiv3",
"phonenumber", "phonenumber",
"proc-macro2", "proc-macro2",
@ -1698,7 +1673,7 @@ dependencies = [
"rand", "rand",
"regex", "regex",
"reqwest", "reqwest",
"reqwest-middleware 0.1.6", "reqwest-middleware",
"rustfmt-wrapper", "rustfmt-wrapper",
"schemars", "schemars",
"serde", "serde",
@ -1711,18 +1686,17 @@ dependencies = [
"slog-stdlog", "slog-stdlog",
"slog-term", "slog-term",
"thiserror", "thiserror",
"tokio",
"url", "url",
"uuid", "uuid",
] ]
[[package]] [[package]]
name = "openapiv3" name = "openapiv3"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1a9f106eb0a780abd17ba9fca8e0843e3461630bcbe2af0ad4d5d3ba4e9aa4" checksum = "75e56d5c441965b6425165b7e3223cc933ca469834f4a8b4786817a1f9dc4f13"
dependencies = [ dependencies = [
"indexmap 1.9.3", "indexmap 2.0.0",
"serde", "serde",
"serde_json", "serde_json",
] ]
@ -1752,12 +1726,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "os_str_bytes"
version = "6.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.2" version = "0.11.2"
@ -2183,26 +2151,10 @@ checksum = "59e50a2e70970896c99d1b8f20ddc30a70b30d3ac6e619a03a8353b64a49b277"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"reqwest", "reqwest",
"reqwest-middleware 0.2.3", "reqwest-middleware",
"task-local-extensions", "task-local-extensions",
] ]
[[package]]
name = "reqwest-middleware"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69539cea4148dce683bec9dc95be3f0397a9bb2c248a49c8296a9d21659a8cdd"
dependencies = [
"anyhow",
"async-trait",
"futures",
"http",
"reqwest",
"serde",
"task-local-extensions",
"thiserror",
]
[[package]] [[package]]
name = "reqwest-middleware" name = "reqwest-middleware"
version = "0.2.3" version = "0.2.3"
@ -2233,7 +2185,7 @@ dependencies = [
"hyper", "hyper",
"parking_lot 0.11.2", "parking_lot 0.11.2",
"reqwest", "reqwest",
"reqwest-middleware 0.2.3", "reqwest-middleware",
"retry-policies", "retry-policies",
"task-local-extensions", "task-local-extensions",
"tokio", "tokio",
@ -2253,7 +2205,7 @@ dependencies = [
"matchit", "matchit",
"opentelemetry", "opentelemetry",
"reqwest", "reqwest",
"reqwest-middleware 0.2.3", "reqwest-middleware",
"task-local-extensions", "task-local-extensions",
"tracing", "tracing",
"tracing-opentelemetry", "tracing-opentelemetry",
@ -2310,6 +2262,20 @@ dependencies = [
"toolchain_find", "toolchain_find",
] ]
[[package]]
name = "rustix"
version = "0.37.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
dependencies = [
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys 0.3.8",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.9" version = "0.38.9"
@ -2319,7 +2285,7 @@ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys 0.4.5",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -2677,7 +2643,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"slog", "slog",
"time 0.3.27", "time",
] ]
[[package]] [[package]]
@ -2712,7 +2678,7 @@ dependencies = [
"slog", "slog",
"term", "term",
"thread_local", "thread_local",
"time 0.3.27", "time",
] ]
[[package]] [[package]]
@ -2837,7 +2803,7 @@ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"rustix", "rustix 0.38.9",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -2862,12 +2828,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "textwrap" name = "terminal_size"
version = "0.16.0" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
dependencies = [ dependencies = [
"unicode-width", "rustix 0.37.23",
"windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -2911,17 +2878,6 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.27" version = "0.3.27"
@ -3400,12 +3356,6 @@ dependencies = [
"try-lock", "try-lock",
] ]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

@ -20,5 +20,5 @@ syn = { version = "2.0.33", features = ["full"] }
[dev-dependencies] [dev-dependencies]
expectorate = "1.0.7" expectorate = "1.0.7"
openapitor = "0.0.5" openapitor = { git = "https://github.com/KittyCAD/kittycad.rs", branch = "main" }
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"

View File

@ -348,7 +348,7 @@ impl SourceRange {
} }
} }
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)] #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
pub struct Point2d { pub struct Point2d {
pub x: f64, pub x: f64,
@ -379,6 +379,16 @@ impl From<Point2d> for kittycad::types::Point2D {
} }
} }
impl Point2d {
pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
pub fn scale(self, scalar: f64) -> Self {
Self {
x: self.x * scalar,
y: self.y * scalar,
}
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)] #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
pub struct Point3d { pub struct Point3d {

View File

@ -7,7 +7,7 @@ use schemars::JsonSchema;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{MemoryItem, SketchGroup}, executor::{MemoryItem, SketchGroup},
std::{utils::get_angle, Args}, std::{utils::Angle, Args},
}; };
/// Returns the segment end of x. /// Returns the segment end of x.
@ -174,9 +174,9 @@ fn inner_segment_angle(segment_name: &str, sketch_group: SketchGroup, args: &mut
})?; })?;
let line = path.get_base(); let line = path.get_base();
let result = get_angle(&line.from, &line.to); let result = Angle::between(line.from.into(), line.to.into());
Ok(result) Ok(result.degrees())
} }
/// Returns the angle to match the given length for x. /// Returns the angle to match the given length for x.

View File

@ -15,6 +15,8 @@ use crate::{
}, },
}; };
use super::utils::Angle;
/// Data to draw a line to a point. /// Data to draw a line to a point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
@ -392,13 +394,13 @@ fn inner_angled_line_of_x_length(
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]), AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
}; };
let to = get_y_component(angle, length); let to = get_y_component(Angle::from_degrees(angle), length);
let new_sketch_group = inner_line( let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data { if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag { to, tag } LineData::PointWithTag { to: to.into(), tag }
} else { } else {
LineData::Point(to) LineData::Point(to.into())
}, },
sketch_group, sketch_group,
args, args,
@ -487,13 +489,13 @@ fn inner_angled_line_of_y_length(
AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]), AngledLineData::AngleAndLength(angle_and_length) => (angle_and_length[0], angle_and_length[1]),
}; };
let to = get_x_component(angle, length); let to = get_x_component(Angle::from_degrees(angle), length);
let new_sketch_group = inner_line( let new_sketch_group = inner_line(
if let AngledLineData::AngleWithTag { tag, .. } = data { if let AngledLineData::AngleWithTag { tag, .. } = data {
LineData::PointWithTag { to, tag } LineData::PointWithTag { to: to.into(), tag }
} else { } else {
LineData::Point(to) LineData::Point(to.into())
}, },
sketch_group, sketch_group,
args, args,
@ -588,16 +590,16 @@ fn inner_angled_line_that_intersects(
let from = sketch_group.get_coords_from_paths()?; let from = sketch_group.get_coords_from_paths()?;
let to = intersection_with_parallel_line( let to = intersection_with_parallel_line(
&[intersect_path.from, intersect_path.to], &[intersect_path.from.into(), intersect_path.to.into()],
data.offset.unwrap_or_default(), data.offset.unwrap_or_default(),
data.angle, data.angle,
from.into(), from,
); );
let line_to_data = if let Some(tag) = data.tag { let line_to_data = if let Some(tag) = data.tag {
LineToData::PointWithTag { to, tag } LineToData::PointWithTag { to: to.into(), tag }
} else { } else {
LineToData::Point(to) LineToData::Point(to.into())
}; };
let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?; let new_sketch_group = inner_line_to(line_to_data, sketch_group, args)?;
@ -766,7 +768,7 @@ pub fn arc(args: &mut Args) -> Result<MemoryItem, KclError> {
name = "arc", name = "arc",
}] }]
fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> { fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Result<SketchGroup, KclError> {
let from = sketch_group.get_coords_from_paths()?; let from: Point2d = sketch_group.get_coords_from_paths()?;
let (center, angle_start, angle_end, radius, end) = match &data { let (center, angle_start, angle_end, radius, end) = match &data {
ArcData::AnglesAndRadiusWithTag { ArcData::AnglesAndRadiusWithTag {
@ -775,23 +777,27 @@ fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Resul
radius, radius,
.. ..
} => { } => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius); let a_start = Angle::from_degrees(*angle_start);
(center, *angle_start, *angle_end, *radius, end) let a_end = Angle::from_degrees(*angle_end);
let (center, end) = arc_center_and_end(from, a_start, a_end, *radius);
(center, a_start, a_end, *radius, end)
} }
ArcData::AnglesAndRadius { ArcData::AnglesAndRadius {
angle_start, angle_start,
angle_end, angle_end,
radius, radius,
} => { } => {
let (center, end) = arc_center_and_end(&from, *angle_start, *angle_end, *radius); let a_start = Angle::from_degrees(*angle_start);
(center, *angle_start, *angle_end, *radius, end) let a_end = Angle::from_degrees(*angle_end);
let (center, end) = arc_center_and_end(from, a_start, a_end, *radius);
(center, a_start, a_end, *radius, end)
} }
ArcData::CenterToRadiusWithTag { center, to, radius, .. } => { ArcData::CenterToRadiusWithTag { center, to, radius, .. } => {
let (angle_start, angle_end) = arc_angles(&from, &center.into(), &to.into(), *radius, args.source_range)?; let (angle_start, angle_end) = arc_angles(from, center.into(), to.into(), *radius, args.source_range)?;
(center.into(), angle_start, angle_end, *radius, to.into()) (center.into(), angle_start, angle_end, *radius, to.into())
} }
ArcData::CenterToRadius { center, to, radius } => { ArcData::CenterToRadius { center, to, radius } => {
let (angle_start, angle_end) = arc_angles(&from, &center.into(), &to.into(), *radius, args.source_range)?; let (angle_start, angle_end) = arc_angles(from, center.into(), to.into(), *radius, args.source_range)?;
(center.into(), angle_start, angle_end, *radius, to.into()) (center.into(), angle_start, angle_end, *radius, to.into())
} }
}; };
@ -803,8 +809,8 @@ fn inner_arc(data: ArcData, sketch_group: SketchGroup, args: &mut Args) -> Resul
ModelingCmd::ExtendPath { ModelingCmd::ExtendPath {
path: sketch_group.id, path: sketch_group.id,
segment: kittycad::types::PathSegment::Arc { segment: kittycad::types::PathSegment::Arc {
angle_start, angle_start: angle_start.degrees(),
angle_end, angle_end: angle_end.degrees(),
center: center.into(), center: center.into(),
radius, radius,
}, },

View File

@ -1,30 +1,85 @@
use std::f64::consts::PI;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{Point2d, SourceRange}, executor::{Point2d, SourceRange},
}; };
pub fn get_angle(a: &[f64; 2], b: &[f64; 2]) -> f64 { #[derive(Clone, Copy, Default, PartialOrd, PartialEq, Debug)]
let x = b[0] - a[0]; pub struct Angle {
let y = b[1] - a[1]; degrees: f64,
normalise_angle(y.atan2(x).to_degrees())
} }
pub fn normalise_angle(angle: f64) -> f64 { impl Angle {
let result = ((angle % 360.0) + 360.0) % 360.0; const ZERO: Self = Self { degrees: 0.0 };
if result > 180.0 { /// Make an angle of the given degrees.
result - 360.0 pub fn from_degrees(degrees: f64) -> Self {
} else { Self { degrees }
result }
/// Make an angle of the given radians.
pub fn from_radians(radians: f64) -> Self {
Self::from_degrees(radians.to_degrees())
}
/// Get the angle in degrees
pub fn degrees(&self) -> f64 {
self.degrees
}
/// Get the angle in radians
pub fn radians(&self) -> f64 {
self.degrees.to_radians()
}
/// Get the angle between these points
pub fn between(a: Point2d, b: Point2d) -> Self {
let x = b.x - a.x;
let y = b.y - a.y;
Self::from_radians(y.atan2(x)).normalize()
}
/// Normalize the angle
pub fn normalize(self) -> Self {
let angle = self.degrees();
let result = ((angle % 360.0) + 360.0) % 360.0;
Self::from_degrees(if result > 180.0 { result - 360.0 } else { result })
}
/// Gives the ▲-angle between from and to angles (shortest path), use radians.
///
/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
/// # Examples
///
/// ```
/// use std::f64::consts::PI;
/// use kcl_lib::std::utils::Angle;
///
/// assert_eq!(
/// Angle::delta(Angle::from_radians(PI / 8.0), Angle::from_radians(PI / 4.0)),
/// Angle::from_radians(PI / 8.0)
/// );
/// ```
#[allow(dead_code)]
pub fn delta(from_angle: Self, to_angle: Self) -> Self {
let norm_from_angle = normalize_rad(from_angle.radians());
let norm_to_angle = normalize_rad(to_angle.radians());
let provisional = norm_to_angle - norm_from_angle;
if provisional > -PI && provisional <= PI {
return Angle::from_radians(provisional);
}
if provisional > PI {
return Angle::from_radians(provisional - 2.0 * PI);
}
if provisional < -PI {
return Angle::from_radians(provisional + 2.0 * PI);
}
Angle::ZERO
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn clockwise_sign(points: &[[f64; 2]]) -> i32 { pub fn clockwise_sign(points: &[Point2d]) -> i32 {
let mut sum = 0.0; let mut sum = 0.0;
for i in 0..points.len() { for i in 0..points.len() {
let current_point = points[i]; let current_point = points[i];
let next_point = points[(i + 1) % points.len()]; let next_point = points[(i + 1) % points.len()];
sum += (next_point[0] - current_point[0]) * (next_point[1] + current_point[1]); sum += (next_point.x - current_point.x) * (next_point.y + current_point.y);
} }
if sum >= 0.0 { if sum >= 0.0 {
1 1
@ -35,139 +90,145 @@ pub fn clockwise_sign(points: &[[f64; 2]]) -> i32 {
#[allow(dead_code)] #[allow(dead_code)]
pub fn normalize_rad(angle: f64) -> f64 { pub fn normalize_rad(angle: f64) -> f64 {
let draft = angle % (2.0 * std::f64::consts::PI); let draft = angle % (2.0 * PI);
if draft < 0.0 { if draft < 0.0 {
draft + 2.0 * std::f64::consts::PI draft + 2.0 * PI
} else { } else {
draft draft
} }
} }
/// Gives the ▲-angle between from and to angles (shortest path), use radians.
///
/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
/// # Examples
///
/// ```
/// assert_eq!(
/// kcl_lib::std::utils::delta_angle(std::f64::consts::PI / 8.0, std::f64::consts::PI / 4.0),
/// std::f64::consts::PI / 8.0
/// );
/// ```
#[allow(dead_code)]
pub fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
let norm_from_angle = normalize_rad(from_angle);
let norm_to_angle = normalize_rad(to_angle);
let provisional = norm_to_angle - norm_from_angle;
if provisional > -std::f64::consts::PI && provisional <= std::f64::consts::PI {
return provisional;
}
if provisional > std::f64::consts::PI {
return provisional - 2.0 * std::f64::consts::PI;
}
if provisional < -std::f64::consts::PI {
return provisional + 2.0 * std::f64::consts::PI;
}
0.0
}
/// Calculates the distance between two points. /// Calculates the distance between two points.
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use kcl_lib::executor::Point2d;
///
/// assert_eq!( /// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[0.0, 5.0]), /// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d{x: 0.0, y: 5.0}),
/// 5.0 /// 5.0
/// ); /// );
/// assert_eq!( /// assert_eq!(
/// kcl_lib::std::utils::distance_between_points(&[0.0, 0.0], &[3.0, 4.0]), /// kcl_lib::std::utils::distance_between_points(Point2d::ZERO, Point2d{x: 3.0, y: 4.0}),
/// 5.0 /// 5.0
/// ); /// );
/// ``` /// ```
#[allow(dead_code)] #[allow(dead_code)]
pub fn distance_between_points(point_a: &[f64; 2], point_b: &[f64; 2]) -> f64 { pub fn distance_between_points(point_a: Point2d, point_b: Point2d) -> f64 {
let x1 = point_a[0]; let x1 = point_a.x;
let y1 = point_a[1]; let y1 = point_a.y;
let x2 = point_b[0]; let x2 = point_b.x;
let y2 = point_b[1]; let y2 = point_b.y;
((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt() ((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt()
} }
pub fn calculate_intersection_of_two_lines(line1: &[[f64; 2]; 2], line2_angle: f64, line2_point: [f64; 2]) -> [f64; 2] { pub fn calculate_intersection_of_two_lines(line1: &[Point2d; 2], line2_angle: f64, line2_point: Point2d) -> Point2d {
let line2_point_b = [ let line2_point_b = Point2d {
line2_point[0] + f64::cos(line2_angle.to_radians()) * 10.0, x: line2_point.x + f64::cos(line2_angle.to_radians()) * 10.0,
line2_point[1] + f64::sin(line2_angle.to_radians()) * 10.0, y: line2_point.y + f64::sin(line2_angle.to_radians()) * 10.0,
]; };
intersect(line1[0], line1[1], line2_point, line2_point_b) intersect(line1[0], line1[1], line2_point, line2_point_b)
} }
pub fn intersect(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2], p4: [f64; 2]) -> [f64; 2] { pub fn intersect(p1: Point2d, p2: Point2d, p3: Point2d, p4: Point2d) -> Point2d {
let slope = |p1: [f64; 2], p2: [f64; 2]| (p1[1] - p2[1]) / (p1[0] - p2[0]); let slope = |p1: Point2d, p2: Point2d| (p1.y - p2.y) / (p1.x - p2.x);
let constant = |p1: [f64; 2], p2: [f64; 2]| p1[1] - slope(p1, p2) * p1[0]; let constant = |p1: Point2d, p2: Point2d| p1.y - slope(p1, p2) * p1.x;
let get_y = |for_x: f64, p1: [f64; 2], p2: [f64; 2]| slope(p1, p2) * for_x + constant(p1, p2); let get_y = |for_x: f64, p1: Point2d, p2: Point2d| slope(p1, p2) * for_x + constant(p1, p2);
if p1[0] == p2[0] { if p1.x == p2.x {
return [p1[0], get_y(p1[0], p3, p4)]; return Point2d {
x: p1.x,
y: get_y(p1.x, p3, p4),
};
} }
if p3[0] == p4[0] { if p3.x == p4.x {
return [p3[0], get_y(p3[0], p1, p2)]; return Point2d {
x: p3.x,
y: get_y(p3.x, p1, p2),
};
} }
let x = (constant(p3, p4) - constant(p1, p2)) / (slope(p1, p2) - slope(p3, p4)); let x = (constant(p3, p4) - constant(p1, p2)) / (slope(p1, p2) - slope(p3, p4));
let y = get_y(x, p1, p2); let y = get_y(x, p1, p2);
[x, y] Point2d { x, y }
} }
pub fn intersection_with_parallel_line( pub fn intersection_with_parallel_line(
line1: &[[f64; 2]; 2], line1: &[Point2d; 2],
line1_offset: f64, line1_offset: f64,
line2_angle: f64, line2_angle: f64,
line2_point: [f64; 2], line2_point: Point2d,
) -> [f64; 2] { ) -> Point2d {
calculate_intersection_of_two_lines(&offset_line(line1_offset, line1[0], line1[1]), line2_angle, line2_point) calculate_intersection_of_two_lines(&offset_line(line1_offset, line1[0], line1[1]), line2_angle, line2_point)
} }
fn offset_line(offset: f64, p1: [f64; 2], p2: [f64; 2]) -> [[f64; 2]; 2] { fn offset_line(offset: f64, p1: Point2d, p2: Point2d) -> [Point2d; 2] {
if p1[0] == p2[0] { if p1.x == p2.x {
let direction = (p1[1] - p2[1]).signum(); let direction = (p1.y - p2.y).signum();
return [[p1[0] + offset * direction, p1[1]], [p2[0] + offset * direction, p2[1]]]; return [
Point2d {
x: p1.x + offset * direction,
y: p1.y,
},
Point2d {
x: p2.x + offset * direction,
y: p2.y,
},
];
} }
if p1[1] == p2[1] { if p1.y == p2.y {
let direction = (p2[0] - p1[0]).signum(); let direction = (p2.x - p1.x).signum();
return [[p1[0], p1[1] + offset * direction], [p2[0], p2[1] + offset * direction]]; return [
Point2d {
x: p1.x,
y: p1.y + offset * direction,
},
Point2d {
x: p2.x,
y: p2.y + offset * direction,
},
];
} }
let x_offset = offset / f64::sin(f64::atan2(p1[1] - p2[1], p1[0] - p2[0])); let x_offset = offset / f64::sin(f64::atan2(p1.y - p2.y, p1.x - p2.x));
[[p1[0] + x_offset, p1[1]], [p2[0] + x_offset, p2[1]]] [
Point2d {
x: p1.x + x_offset,
y: p1.y,
},
Point2d {
x: p2.x + x_offset,
y: p2.y,
},
]
} }
pub fn get_y_component(angle_degree: f64, x_component: f64) -> [f64; 2] { pub fn get_y_component(angle: Angle, x: f64) -> Point2d {
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360 let normalised_angle = ((angle.degrees() % 360.0) + 360.0) % 360.0; // between 0 and 360
let y_component = x_component * f64::tan(normalised_angle.to_radians()); let y = x * f64::tan(normalised_angle.to_radians());
let sign = if normalised_angle > 90.0 && normalised_angle <= 270.0 { let sign = if normalised_angle > 90.0 && normalised_angle <= 270.0 {
-1.0 -1.0
} else { } else {
1.0 1.0
}; };
[sign * x_component, sign * y_component] Point2d { x, y }.scale(sign)
} }
pub fn get_x_component(angle_degree: f64, y_component: f64) -> [f64; 2] { pub fn get_x_component(angle: Angle, y: f64) -> Point2d {
let normalised_angle = ((angle_degree % 360.0) + 360.0) % 360.0; // between 0 and 360 let normalised_angle = ((angle.degrees() % 360.0) + 360.0) % 360.0; // between 0 and 360
let x_component = y_component / f64::tan(normalised_angle.to_radians()); let x = y / f64::tan(normalised_angle.to_radians());
let sign = if normalised_angle > 180.0 && normalised_angle <= 360.0 { let sign = if normalised_angle > 180.0 && normalised_angle <= 360.0 {
-1.0 -1.0
} else { } else {
1.0 1.0
}; };
[sign * x_component, sign * y_component] Point2d { x, y }.scale(sign)
} }
pub fn arc_center_and_end(from: &Point2d, start_angle_deg: f64, end_angle_deg: f64, radius: f64) -> (Point2d, Point2d) { pub fn arc_center_and_end(from: Point2d, start_angle: Angle, end_angle: Angle, radius: f64) -> (Point2d, Point2d) {
let start_angle = start_angle_deg.to_radians(); let start_angle = start_angle.radians();
let end_angle = end_angle_deg.to_radians(); let end_angle = end_angle.radians();
let center = Point2d { let center = Point2d {
x: -1.0 * (radius * start_angle.cos() - from.x), x: -1.0 * (radius * start_angle.cos() - from.x),
@ -183,12 +244,12 @@ pub fn arc_center_and_end(from: &Point2d, start_angle_deg: f64, end_angle_deg: f
} }
pub fn arc_angles( pub fn arc_angles(
from: &Point2d, from: Point2d,
to: &Point2d, to: Point2d,
center: &Point2d, center: Point2d,
radius: f64, radius: f64,
source_range: SourceRange, source_range: SourceRange,
) -> Result<(f64, f64), KclError> { ) -> Result<(Angle, Angle), KclError> {
// First make sure that the points are on the circumference of the circle. // First make sure that the points are on the circumference of the circle.
// If not, we'll return an error. // If not, we'll return an error.
if !is_on_circumference(center, from, radius) { if !is_on_circumference(center, from, radius) {
@ -214,13 +275,10 @@ pub fn arc_angles(
let start_angle = (from.y - center.y).atan2(from.x - center.x); let start_angle = (from.y - center.y).atan2(from.x - center.x);
let end_angle = (to.y - center.y).atan2(to.x - center.x); let end_angle = (to.y - center.y).atan2(to.x - center.x);
let start_angle_deg = start_angle.to_degrees(); Ok((Angle::from_radians(start_angle), Angle::from_radians(end_angle)))
let end_angle_deg = end_angle.to_degrees();
Ok((start_angle_deg, end_angle_deg))
} }
pub fn is_on_circumference(center: &Point2d, point: &Point2d, radius: f64) -> bool { pub fn is_on_circumference(center: Point2d, point: Point2d, radius: f64) -> bool {
let dx = point.x - center.x; let dx = point.x - center.x;
let dy = point.y - center.y; let dy = point.y - center.y;
@ -237,7 +295,7 @@ mod tests {
// Here you can bring your functions into scope // Here you can bring your functions into scope
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::{get_x_component, get_y_component}; use super::{get_x_component, get_y_component, Angle};
use crate::executor::SourceRange; use crate::executor::SourceRange;
static EACH_QUAD: [(i32, [i32; 2]); 12] = [ static EACH_QUAD: [(i32, [i32; 2]); 12] = [
@ -261,28 +319,28 @@ mod tests {
let mut results = Vec::new(); let mut results = Vec::new();
for &(angle, expected_result) in EACH_QUAD.iter() { for &(angle, expected_result) in EACH_QUAD.iter() {
let res = get_y_component(angle as f64, 1.0); let res = get_y_component(Angle::from_degrees(angle as f64), 1.0);
results.push([res[0].round() as i32, res[1].round() as i32]); results.push([res.x.round() as i32, res.y.round() as i32]);
expected.push(expected_result); expected.push(expected_result);
} }
assert_eq!(results, expected); assert_eq!(results, expected);
let result = get_y_component(0.0, 1.0); let result = get_y_component(Angle::ZERO, 1.0);
assert_eq!(result[0] as i32, 1); assert_eq!(result.x as i32, 1);
assert_eq!(result[1] as i32, 0); assert_eq!(result.y as i32, 0);
let result = get_y_component(90.0, 1.0); let result = get_y_component(Angle::from_degrees(90.0), 1.0);
assert_eq!(result[0] as i32, 1); assert_eq!(result.x as i32, 1);
assert!(result[1] > 100000.0); assert!(result.y > 100000.0);
let result = get_y_component(180.0, 1.0); let result = get_y_component(Angle::from_degrees(180.0), 1.0);
assert_eq!(result[0] as i32, -1); assert_eq!(result.x as i32, -1);
assert!((result[1] - 0.0).abs() < f64::EPSILON); assert!((result.y - 0.0).abs() < f64::EPSILON);
let result = get_y_component(270.0, 1.0); let result = get_y_component(Angle::from_degrees(270.0), 1.0);
assert_eq!(result[0] as i32, -1); assert_eq!(result.x as i32, -1);
assert!(result[1] < -100000.0); assert!(result.y < -100000.0);
} }
#[test] #[test]
@ -291,45 +349,60 @@ mod tests {
let mut results = Vec::new(); let mut results = Vec::new();
for &(angle, expected_result) in EACH_QUAD.iter() { for &(angle, expected_result) in EACH_QUAD.iter() {
let res = get_x_component(angle as f64, 1.0); let res = get_x_component(Angle::from_degrees(angle as f64), 1.0);
results.push([res[0].round() as i32, res[1].round() as i32]); results.push([res.x.round() as i32, res.y.round() as i32]);
expected.push(expected_result); expected.push(expected_result);
} }
assert_eq!(results, expected); assert_eq!(results, expected);
let result = get_x_component(0.0, 1.0); let result = get_x_component(Angle::ZERO, 1.0);
assert!(result[0] > 100000.0); assert!(result.x > 100000.0);
assert_eq!(result[1] as i32, 1); assert_eq!(result.y as i32, 1);
let result = get_x_component(90.0, 1.0); let result = get_x_component(Angle::from_degrees(90.0), 1.0);
assert!((result[0] - 0.0).abs() < f64::EPSILON); assert!((result.x - 0.0).abs() < f64::EPSILON);
assert_eq!(result[1] as i32, 1); assert_eq!(result.y as i32, 1);
let result = get_x_component(180.0, 1.0); let result = get_x_component(Angle::from_degrees(180.0), 1.0);
assert!(result[0] < -100000.0); assert!(result.x < -100000.0);
assert_eq!(result[1] as i32, 1); assert_eq!(result.y as i32, 1);
let result = get_x_component(270.0, 1.0); let result = get_x_component(Angle::from_degrees(270.0), 1.0);
assert!((result[0] - 0.0).abs() < f64::EPSILON); assert!((result.x - 0.0).abs() < f64::EPSILON);
assert_eq!(result[1] as i32, -1); assert_eq!(result.y as i32, -1);
} }
#[test] #[test]
fn test_arc_center_and_end() { fn test_arc_center_and_end() {
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 90.0, 1.0); let (center, end) = super::arc_center_and_end(
super::Point2d { x: 0.0, y: 0.0 },
Angle::ZERO,
Angle::from_degrees(90.0),
1.0,
);
assert_eq!(center.x.round(), -1.0); assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0); assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -1.0); assert_eq!(end.x.round(), -1.0);
assert_eq!(end.y, 1.0); assert_eq!(end.y, 1.0);
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 180.0, 1.0); let (center, end) = super::arc_center_and_end(
super::Point2d { x: 0.0, y: 0.0 },
Angle::ZERO,
Angle::from_degrees(180.0),
1.0,
);
assert_eq!(center.x.round(), -1.0); assert_eq!(center.x.round(), -1.0);
assert_eq!(center.y, 0.0); assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -2.0); assert_eq!(end.x.round(), -2.0);
assert_eq!(end.y.round(), 0.0); assert_eq!(end.y.round(), 0.0);
let (center, end) = super::arc_center_and_end(&super::Point2d { x: 0.0, y: 0.0 }, 0.0, 180.0, 10.0); let (center, end) = super::arc_center_and_end(
super::Point2d { x: 0.0, y: 0.0 },
Angle::ZERO,
Angle::from_degrees(180.0),
10.0,
);
assert_eq!(center.x.round(), -10.0); assert_eq!(center.x.round(), -10.0);
assert_eq!(center.y, 0.0); assert_eq!(center.y, 0.0);
assert_eq!(end.x.round(), -20.0); assert_eq!(end.x.round(), -20.0);
@ -339,42 +412,42 @@ mod tests {
#[test] #[test]
fn test_arc_angles() { fn test_arc_angles() {
let (angle_start, angle_end) = super::arc_angles( let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 }, super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -1.0, y: 1.0 }, super::Point2d { x: -1.0, y: 1.0 },
&super::Point2d { x: -1.0, y: 0.0 }, super::Point2d { x: -1.0, y: 0.0 },
1.0, 1.0,
SourceRange(Default::default()), SourceRange(Default::default()),
) )
.unwrap(); .unwrap();
assert_eq!(angle_start.round(), 0.0); assert_eq!(angle_start.degrees().round(), 0.0);
assert_eq!(angle_end.round(), 90.0); assert_eq!(angle_end.degrees().round(), 90.0);
let (angle_start, angle_end) = super::arc_angles( let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 }, super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -2.0, y: 0.0 }, super::Point2d { x: -2.0, y: 0.0 },
&super::Point2d { x: -1.0, y: 0.0 }, super::Point2d { x: -1.0, y: 0.0 },
1.0, 1.0,
SourceRange(Default::default()), SourceRange(Default::default()),
) )
.unwrap(); .unwrap();
assert_eq!(angle_start.round(), 0.0); assert_eq!(angle_start.degrees().round(), 0.0);
assert_eq!(angle_end.round(), 180.0); assert_eq!(angle_end.degrees().round(), 180.0);
let (angle_start, angle_end) = super::arc_angles( let (angle_start, angle_end) = super::arc_angles(
&super::Point2d { x: 0.0, y: 0.0 }, super::Point2d { x: 0.0, y: 0.0 },
&super::Point2d { x: -20.0, y: 0.0 }, super::Point2d { x: -20.0, y: 0.0 },
&super::Point2d { x: -10.0, y: 0.0 }, super::Point2d { x: -10.0, y: 0.0 },
10.0, 10.0,
SourceRange(Default::default()), SourceRange(Default::default()),
) )
.unwrap(); .unwrap();
assert_eq!(angle_start.round(), 0.0); assert_eq!(angle_start.degrees().round(), 0.0);
assert_eq!(angle_end.round(), 180.0); assert_eq!(angle_end.degrees().round(), 180.0);
let result = super::arc_angles( let result = super::arc_angles(
&super::Point2d { x: 0.0, y: 5.0 }, super::Point2d { x: 0.0, y: 5.0 },
&super::Point2d { x: 5.0, y: 5.0 }, super::Point2d { x: 5.0, y: 5.0 },
&super::Point2d { x: 10.0, y: -10.0 }, super::Point2d { x: 10.0, y: -10.0 },
10.0, 10.0,
SourceRange(Default::default()), SourceRange(Default::default()),
); );
@ -384,7 +457,7 @@ mod tests {
} else { } else {
panic!("Expected error"); panic!("Expected error");
} }
assert_eq!(angle_start.round(), 0.0); assert_eq!(angle_start.degrees().round(), 0.0);
assert_eq!(angle_end.round(), 180.0); assert_eq!(angle_end.degrees().round(), 180.0);
} }
} }