Guptaarnav 2024 10 28 (#4341)

* accessing toast error correctly

* wrapping try-catch around fs.stat on cli arg

* implemented array push

* changing arg execution order for sketch arc

* addressing sketchFromKclValue error for Sketches in Uservals

* addressing 'update to He inside a test not wrapped in act(...' error

* yarn fmt fix

* implemented polygon stdlib function

* changing polygon inscribed arg description in docs

* addressing cargo clippy warning

* Add tangential arc unavailable reason tooltip

* fixing tsc errors

* preventing hidden dirs from showing up as projects and prohibits renaming projects as hidden

* adding unit test for desktop listProjects

* showing no completions when last typed word is a number

* fmt

* Make clippy happy

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

* yarn tsc fix: added missing toast import in Home.tsx

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

* regenerating markdown docs for incoming merge from main

---------

Co-authored-by: arnav <arnav@agupta.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
49fl
2024-10-28 20:52:51 -04:00
committed by GitHub
parent 81279aa4e8
commit 7103ded32a
28 changed files with 17111 additions and 27 deletions

View File

@ -74,10 +74,12 @@ layout: manual
* [`patternTransform`](kcl/patternTransform) * [`patternTransform`](kcl/patternTransform)
* [`pi`](kcl/pi) * [`pi`](kcl/pi)
* [`polar`](kcl/polar) * [`polar`](kcl/polar)
* [`polygon`](kcl/polygon)
* [`pow`](kcl/pow) * [`pow`](kcl/pow)
* [`profileStart`](kcl/profileStart) * [`profileStart`](kcl/profileStart)
* [`profileStartX`](kcl/profileStartX) * [`profileStartX`](kcl/profileStartX)
* [`profileStartY`](kcl/profileStartY) * [`profileStartY`](kcl/profileStartY)
* [`push`](kcl/push)
* [`reduce`](kcl/reduce) * [`reduce`](kcl/reduce)
* [`rem`](kcl/rem) * [`rem`](kcl/rem)
* [`revolve`](kcl/revolve) * [`revolve`](kcl/revolve)

60
docs/kcl/polygon.md Normal file

File diff suppressed because one or more lines are too long

38
docs/kcl/push.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
---
title: "PolygonData"
excerpt: "Data for drawing a polygon"
layout: manual
---
Data for drawing a polygon
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `radius` |`number`| The radius of the polygon | No |
| `numSides` |`integer`| The number of sides in the polygon | No |
| `center` |`[number, number]`| The center point of the polygon | No |
| `inscribed` |`boolean`| Whether the polygon is inscribed (true) or circumscribed (false) about a circle with the specified radius | No |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -100,6 +100,11 @@ export function Toolbar({
function resolveItemConfig( function resolveItemConfig(
maybeIconConfig: ToolbarItem maybeIconConfig: ToolbarItem
): ToolbarItemResolved { ): ToolbarItemResolved {
const isDisabled =
disableAllButtons ||
maybeIconConfig.status !== 'available' ||
maybeIconConfig.disabled?.(state) === true
return { return {
...maybeIconConfig, ...maybeIconConfig,
title: title:
@ -113,10 +118,11 @@ export function Toolbar({
typeof maybeIconConfig.hotkey === 'string' typeof maybeIconConfig.hotkey === 'string'
? maybeIconConfig.hotkey ? maybeIconConfig.hotkey
: maybeIconConfig.hotkey?.(state), : maybeIconConfig.hotkey?.(state),
disabled: disabled: isDisabled,
disableAllButtons || disabledReason:
maybeIconConfig.status !== 'available' || typeof maybeIconConfig.disabledReason === 'function'
maybeIconConfig.disabled?.(state) === true, ? maybeIconConfig.disabledReason(state)
: maybeIconConfig.disabledReason,
disableHotkey: maybeIconConfig.disableHotkey?.(state), disableHotkey: maybeIconConfig.disableHotkey?.(state),
status: maybeIconConfig.status, status: maybeIconConfig.status,
} }
@ -273,6 +279,8 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
itemConfig: ToolbarItemResolved itemConfig: ToolbarItemResolved
configCallbackProps: ToolbarItemCallbackProps configCallbackProps: ToolbarItemCallbackProps
}) { }) {
const { state } = useModelingContext()
useHotkeys( useHotkeys(
itemConfig.hotkey || '', itemConfig.hotkey || '',
() => { () => {
@ -336,6 +344,17 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
)} )}
</div> </div>
<p className="px-2 text-ch font-sans">{itemConfig.description}</p> <p className="px-2 text-ch font-sans">{itemConfig.description}</p>
{/* Add disabled reason if item is disabled */}
{itemConfig.disabled && itemConfig.disabledReason && (
<>
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
<p className="px-2 text-ch font-sans text-chalkboard-70 dark:text-chalkboard-40">
{typeof itemConfig.disabledReason === 'function'
? itemConfig.disabledReason(state)
: itemConfig.disabledReason}
</p>
</>
)}
{itemConfig.links.length > 0 && ( {itemConfig.links.length > 0 && (
<> <>
<hr className="border-chalkboard-20 dark:border-chalkboard-80" /> <hr className="border-chalkboard-20 dark:border-chalkboard-80" />

View File

@ -88,8 +88,12 @@ export const MemoryPane = () => {
export const processMemory = (programMemory: ProgramMemory) => { export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {} const processedMemory: any = {}
for (const [key, val] of programMemory?.visibleEntries()) { for (const [key, val] of programMemory?.visibleEntries()) {
if (typeof val.value !== 'function') { if (
const sg = sketchFromKclValue(val, null) (val.type === 'UserVal' && val.value.type === 'Sketch') ||
// @ts-ignore
(val.type !== 'Function' && val.type !== 'UserVal')
) {
const sg = sketchFromKclValue(val, key)
if (val.type === 'Solid') { if (val.type === 'Solid') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => { processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest return rest
@ -98,15 +102,16 @@ export const processMemory = (programMemory: ProgramMemory) => {
processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => { processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => {
return rest return rest
}) })
} else if ((val.type as any) === 'Function') {
processedMemory[key] = `__function(${(val as any)?.expression?.params
?.map?.(({ identifier }: any) => identifier?.name || '')
.join(', ')})__`
} else { } else {
processedMemory[key] = val.value processedMemory[key] = val.value
} }
} else if (key !== 'log') { //@ts-ignore
processedMemory[key] = '__function__' } else if (val.type === 'Function') {
processedMemory[key] = `__function(${(val as any)?.expression?.params
?.map?.(({ identifier }: any) => identifier?.name || '')
.join(', ')})__`
} else {
processedMemory[key] = val.value
} }
} }
return processedMemory return processedMemory

View File

@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react' import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import UserSidebarMenu from './UserSidebarMenu' import UserSidebarMenu from './UserSidebarMenu'
import { import {
Route, Route,
@ -13,7 +13,7 @@ import { CommandBarProvider } from './CommandBar/CommandBarProvider'
type User = Models['User_type'] type User = Models['User_type']
describe('UserSidebarMenu tests', () => { describe('UserSidebarMenu tests', () => {
test("Renders user's name and email if available", () => { test("Renders user's name and email if available", async () => {
const userWellFormed: User = { const userWellFormed: User = {
id: '8675309', id: '8675309',
name: 'Test User', name: 'Test User',
@ -39,13 +39,19 @@ describe('UserSidebarMenu tests', () => {
fireEvent.click(screen.getByTestId('user-sidebar-toggle')) fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
expect(screen.getByTestId('username')).toHaveTextContent( await waitFor(() => {
userWellFormed.name || '' expect(screen.getByTestId('username')).toHaveTextContent(
) userWellFormed.name || ''
expect(screen.getByTestId('email')).toHaveTextContent(userWellFormed.email) )
})
await waitFor(() => {
expect(screen.getByTestId('email')).toHaveTextContent(
userWellFormed.email
)
})
}) })
test("Renders just the user's email if no name is available", () => { test("Renders just the user's email if no name is available", async () => {
const userNoName: User = { const userNoName: User = {
id: '8675309', id: '8675309',
email: 'kittycad.sidebar.test@example.com', email: 'kittycad.sidebar.test@example.com',
@ -71,10 +77,12 @@ describe('UserSidebarMenu tests', () => {
fireEvent.click(screen.getByTestId('user-sidebar-toggle')) fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
expect(screen.getByTestId('username')).toHaveTextContent(userNoName.email) await waitFor(() => {
expect(screen.getByTestId('username')).toHaveTextContent(userNoName.email)
})
}) })
test('Renders a menu button if no user avatar is available', () => { test('Renders a menu button if no user avatar is available', async () => {
const userNoAvatar: User = { const userNoAvatar: User = {
id: '8675309', id: '8675309',
name: 'Test User', name: 'Test User',
@ -98,9 +106,11 @@ describe('UserSidebarMenu tests', () => {
</TestWrap> </TestWrap>
) )
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent( await waitFor(() => {
'User menu' expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent(
) 'User menu'
)
})
}) })
}) })

131
src/lib/desktop.test.ts Normal file
View File

@ -0,0 +1,131 @@
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { listProjects } from './desktop'
import { DeepPartial } from './types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
// Mock the electron window global
const mockElectron = {
readdir: vi.fn(),
path: {
join: vi.fn(),
basename: vi.fn(),
dirname: vi.fn(),
},
stat: vi.fn(),
statIsDirectory: vi.fn(),
exists: vi.fn(),
writeFile: vi.fn(),
readFile: vi.fn(),
os: {
isMac: false,
isWindows: false,
},
process: {
env: {},
},
getPath: vi.fn(),
kittycad: vi.fn(),
}
vi.stubGlobal('window', { electron: mockElectron })
describe('desktop utilities', () => {
const mockConfig: DeepPartial<Configuration> = {
settings: {
project: {
directory: '/test/projects',
},
},
}
const mockFileSystem: { [key: string]: string[] } = {
'/test/projects': [
'.hidden-project',
'valid-project',
'.git',
'project-without-kcl-files',
'another-valid-project',
],
'/test/projects/valid-project': ['file1.kcl', 'file2.stp'],
'/test/projects/project-without-kcl-files': ['file3.glb'],
'/test/projects/another-valid-project': ['file4.kcl'],
}
beforeEach(() => {
vi.clearAllMocks()
// Setup default mock implementations
mockElectron.path.join.mockImplementation((...parts: string[]) =>
parts.join('/')
)
mockElectron.path.basename.mockImplementation((path: string) =>
path.split('/').pop()
)
mockElectron.path.dirname.mockImplementation((path: string) =>
path.split('/').slice(0, -1).join('/')
)
// Mock readdir to return the entries for the given path
mockElectron.readdir.mockImplementation(async (path: string) => {
return mockFileSystem[path] || []
})
// Mock statIsDirectory to return true if the path exists in mockFileSystem
mockElectron.statIsDirectory.mockImplementation(async (path: string) => {
return path in mockFileSystem
})
// Mock stat to always resolve with dummy metadata
mockElectron.stat.mockResolvedValue({
mtimeMs: 123,
atimeMs: 456,
ctimeMs: 789,
size: 100,
mode: 0o666,
})
mockElectron.exists.mockResolvedValue(true)
mockElectron.readFile.mockResolvedValue('')
mockElectron.writeFile.mockResolvedValue(undefined)
mockElectron.getPath.mockResolvedValue('/appData')
mockElectron.kittycad.mockResolvedValue({})
})
describe('listProjects', () => {
it('does not list .git directories', async () => {
const projects = await listProjects(mockConfig)
expect(projects.map((p) => p.name)).not.toContain('.git')
})
it('lists projects excluding hidden and without .kcl files', async () => {
const projects = await listProjects(mockConfig)
// Verify only non-dot projects with .kcl files were included
expect(projects.map((p) => p.name)).toEqual([
'valid-project',
'another-valid-project',
])
// Verify we didn't try to get project info for dot directories
expect(mockElectron.stat).not.toHaveBeenCalledWith(
expect.stringContaining('/.hidden-project')
)
expect(mockElectron.stat).not.toHaveBeenCalledWith(
expect.stringContaining('/.git')
)
// Verify that projects without .kcl files are not included
expect(projects.map((p) => p.name)).not.toContain(
'project-without-kcl-files'
)
})
it('handles empty project directory', async () => {
// Adjust mockFileSystem to simulate empty directory
mockFileSystem['/test/projects'] = []
const projects = await listProjects(mockConfig)
expect(projects).toEqual([])
})
})
})

View File

@ -139,6 +139,11 @@ export async function listProjects(
const entries = await window.electron.readdir(projectDir) const entries = await window.electron.readdir(projectDir)
for (let entry of entries) { for (let entry of entries) {
// Skip directories that start with a dot
if (entry.startsWith('.')) {
continue
}
const projectPath = window.electron.path.join(projectDir, entry) const projectPath = window.electron.path.join(projectDir, entry)
// if it's not a directory ignore. // if it's not a directory ignore.
const isDirectory = await window.electron.statIsDirectory(projectPath) const isDirectory = await window.electron.statIsDirectory(projectPath)

View File

@ -1,5 +1,6 @@
import * as path from 'path' import * as path from 'path'
import * as fs from 'fs/promises' import * as fs from 'fs/promises'
import { Stats } from 'fs'
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import { PROJECT_ENTRYPOINT } from './constants' import { PROJECT_ENTRYPOINT } from './constants'
@ -43,8 +44,16 @@ export default async function getCurrentProjectFile(
? sourcePath ? sourcePath
: path.join(process.cwd(), sourcePath) : path.join(process.cwd(), sourcePath)
let stats: Stats
try {
stats = await fs.stat(sourcePath)
} catch (error) {
return new Error(
`Unable to access the path: ${sourcePath}. Error: ${error}`
)
}
// If the path is a directory, let's assume it is a project directory. // If the path is a directory, let's assume it is a project directory.
const stats = await fs.stat(sourcePath)
if (stats.isDirectory()) { if (stats.isDirectory()) {
// Walk the directory and look for a kcl file. // Walk the directory and look for a kcl file.
const files = await fs.readdir(sourcePath) const files = await fs.readdir(sourcePath)

View File

@ -39,6 +39,9 @@ export type ToolbarItem = {
description: string description: string
links: { label: string; url: string }[] links: { label: string; url: string }[]
isActive?: (state: StateFrom<typeof modelingMachine>) => boolean isActive?: (state: StateFrom<typeof modelingMachine>) => boolean
disabledReason?:
| string
| ((state: StateFrom<typeof modelingMachine>) => string | undefined)
} }
export type ToolbarItemResolved = Omit< export type ToolbarItemResolved = Omit<
@ -349,6 +352,11 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
(!isEditingExistingSketch(state.context) && (!isEditingExistingSketch(state.context) &&
!state.matches({ Sketch: 'Tangential arc to' })) || !state.matches({ Sketch: 'Tangential arc to' })) ||
pipeHasCircle(state.context), pipeHasCircle(state.context),
disabledReason: (state) =>
!isEditingExistingSketch(state.context) &&
!state.matches({ Sketch: 'Tangential arc to' })
? "Cannot start a tangential arc because there's no previous line to be tangential to. Try drawing a line first or selecting an existing sketch to edit."
: undefined,
title: 'Tangential Arc', title: 'Tangential Arc',
hotkey: (state) => hotkey: (state) =>
state.matches({ Sketch: 'Tangential arc to' }) ? ['Esc', 'A'] : 'A', state.matches({ Sketch: 'Tangential arc to' }) ? ['Esc', 'A'] : 'A',

View File

@ -4,6 +4,7 @@ import { AppHeader } from 'components/AppHeader'
import ProjectCard from 'components/ProjectCard/ProjectCard' import ProjectCard from 'components/ProjectCard/ProjectCard'
import { useNavigate, useSearchParams } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { toast } from 'react-hot-toast'
import Loading from 'components/Loading' import Loading from 'components/Loading'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { import {
@ -94,6 +95,11 @@ const Home = () => {
new FormData(e.target as HTMLFormElement) new FormData(e.target as HTMLFormElement)
) )
if (typeof newProjectName === 'string' && newProjectName.startsWith('.')) {
toast.error('Project names cannot start with a dot (.)')
return
}
if (newProjectName !== project.name) { if (newProjectName !== project.name) {
send({ send({
type: 'Rename project', type: 'Rename project',

View File

@ -1041,6 +1041,38 @@ impl LanguageServer for Backend {
tags: None, tags: None,
}]; }];
// Get the current line up to cursor
let Some(current_code) = self
.code_map
.get(params.text_document_position.text_document.uri.as_ref())
else {
return Ok(Some(CompletionResponse::Array(completions)));
};
let Ok(current_code) = std::str::from_utf8(&current_code) else {
return Ok(Some(CompletionResponse::Array(completions)));
};
// Get the current line up to cursor, with bounds checking
if let Some(line) = current_code
.lines()
.nth(params.text_document_position.position.line as usize)
{
let char_pos = params.text_document_position.position.character as usize;
if char_pos <= line.len() {
let line_prefix = &line[..char_pos];
// Get last word
let last_word = line_prefix
.split(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
.last()
.unwrap_or("");
// If the last word starts with a digit, return no completions
if !last_word.is_empty() && last_word.chars().next().unwrap().is_ascii_digit() {
return Ok(None);
}
}
}
completions.extend(self.stdlib_completions.values().cloned()); completions.extend(self.stdlib_completions.values().cloned());
// Add more to the completions if we have more. // Add more to the completions if we have more.

View File

@ -3551,3 +3551,36 @@ const part001 = startSketchOn('XY')
// Check the diagnostics. // Check the diagnostics.
assert_eq!(diagnostics.len(), 2); assert_eq!(diagnostics.len(), 2);
} }
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_kcl_lsp_completions_number_literal() {
let server = kcl_lsp_server(false).await.unwrap();
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: "const thing = 10".to_string(),
},
})
.await;
let completions = server
.completion(tower_lsp::lsp_types::CompletionParams {
text_document_position: tower_lsp::lsp_types::TextDocumentPositionParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
position: tower_lsp::lsp_types::Position { line: 0, character: 15 },
},
context: None,
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
})
.await
.unwrap();
assert_eq!(completions.is_none(), true);
}

View File

@ -459,6 +459,19 @@ impl Args {
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})) }))
} }
pub(crate) fn get_polygon_args(
&self,
) -> Result<
(
crate::std::shapes::PolygonData,
crate::std::shapes::SketchOrSurface,
Option<TagDeclarator>,
),
KclError,
> {
FromArgs::from_args(self, 0)
}
} }
/// Types which impl this trait can be read out of the `Args` passed into a KCL function. /// Types which impl this trait can be read out of the `Args` passed into a KCL function.
@ -652,6 +665,7 @@ impl_from_arg_via_json!(super::sketch::AngledLineData);
impl_from_arg_via_json!(super::sketch::AngledLineToData); impl_from_arg_via_json!(super::sketch::AngledLineToData);
impl_from_arg_via_json!(super::sketch::AngledLineThatIntersectsData); impl_from_arg_via_json!(super::sketch::AngledLineThatIntersectsData);
impl_from_arg_via_json!(super::shapes::CircleData); impl_from_arg_via_json!(super::shapes::CircleData);
impl_from_arg_via_json!(super::shapes::PolygonData);
impl_from_arg_via_json!(super::sketch::ArcData); impl_from_arg_via_json!(super::sketch::ArcData);
impl_from_arg_via_json!(super::sketch::TangentialArcData); impl_from_arg_via_json!(super::sketch::TangentialArcData);
impl_from_arg_via_json!(super::sketch::BezierData); impl_from_arg_via_json!(super::sketch::BezierData);

View File

@ -193,3 +193,63 @@ async fn call_reduce_closure<'a>(
})?; })?;
Ok(out) Ok(out)
} }
/// Append an element to the end of an array.
///
/// Returns a new array with the element appended.
///
/// ```no_run
/// let arr = [1, 2, 3]
/// let new_arr = push(arr, 4)
/// assertEqual(new_arr[3], 4, 0.00001, "4 was added to the end of the array")
/// ```
#[stdlib {
name = "push",
}]
async fn inner_push(array: Vec<KclValue>, elem: KclValue, args: &Args) -> Result<KclValue, KclError> {
// Unwrap the KclValues to JValues for manipulation
let mut unwrapped_array = array
.into_iter()
.map(|k| match k {
KclValue::UserVal(user_val) => Ok(user_val.value),
_ => Err(KclError::Semantic(KclErrorDetails {
message: "Expected a UserVal in array".to_string(),
source_ranges: vec![args.source_range],
})),
})
.collect::<Result<Vec<_>, _>>()?;
// Unwrap the element
let unwrapped_elem = match elem {
KclValue::UserVal(user_val) => user_val.value,
_ => {
return Err(KclError::Semantic(KclErrorDetails {
message: "Expected a UserVal as element".to_string(),
source_ranges: vec![args.source_range],
}));
}
};
// Append the element to the array
unwrapped_array.push(unwrapped_elem);
// Wrap the new array into a UserVal with the source range metadata
let uv = UserVal::new(vec![args.source_range.into()], unwrapped_array);
// Return the new array wrapped as a KclValue::UserVal
Ok(KclValue::UserVal(uv))
}
pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
// Extract the array and the element from the arguments
let (array_jvalues, elem): (Vec<JValue>, KclValue) = FromArgs::from_args(&args, 0)?;
// Convert the array of JValue into Vec<KclValue>
let array: Vec<KclValue> = array_jvalues
.into_iter()
.map(|jval| KclValue::UserVal(UserVal::new(vec![args.source_range.into()], jval)))
.collect();
// Call the inner_push function
inner_push(array, elem, &args).await
}

View File

@ -66,6 +66,7 @@ lazy_static! {
Box::new(crate::std::segment::AngleToMatchLengthX), Box::new(crate::std::segment::AngleToMatchLengthX),
Box::new(crate::std::segment::AngleToMatchLengthY), Box::new(crate::std::segment::AngleToMatchLengthY),
Box::new(crate::std::shapes::Circle), Box::new(crate::std::shapes::Circle),
Box::new(crate::std::shapes::Polygon),
Box::new(crate::std::sketch::LineTo), Box::new(crate::std::sketch::LineTo),
Box::new(crate::std::sketch::Line), Box::new(crate::std::sketch::Line),
Box::new(crate::std::sketch::XLineTo), Box::new(crate::std::sketch::XLineTo),
@ -99,6 +100,7 @@ lazy_static! {
Box::new(crate::std::patterns::PatternTransform), Box::new(crate::std::patterns::PatternTransform),
Box::new(crate::std::array::Reduce), Box::new(crate::std::array::Reduce),
Box::new(crate::std::array::Map), Box::new(crate::std::array::Map),
Box::new(crate::std::array::Push),
Box::new(crate::std::chamfer::Chamfer), Box::new(crate::std::chamfer::Chamfer),
Box::new(crate::std::fillet::Fillet), Box::new(crate::std::fillet::Fillet),
Box::new(crate::std::fillet::GetOppositeEdge), Box::new(crate::std::fillet::GetOppositeEdge),

View File

@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
ast::types::TagDeclarator, ast::types::TagDeclarator,
errors::KclError, errors::{KclError, KclErrorDetails},
executor::{BasePath, ExecState, GeoMeta, KclValue, Path, Sketch, SketchSurface}, executor::{BasePath, ExecState, GeoMeta, KclValue, Path, Sketch, SketchSurface},
std::Args, std::Args,
}; };
@ -24,6 +24,7 @@ use crate::{
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(untagged)] #[serde(untagged)]
pub enum SketchOrSurface { pub enum SketchOrSurface {
SketchSurface(SketchSurface), SketchSurface(SketchSurface),
Sketch(Box<Sketch>), Sketch(Box<Sketch>),
@ -141,3 +142,201 @@ async fn inner_circle(
Ok(new_sketch) Ok(new_sketch)
} }
/// Type of the polygon
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Default)]
#[ts(export)]
#[serde(rename_all = "lowercase")]
pub enum PolygonType {
#[default]
Inscribed,
Circumscribed,
}
/// Data for drawing a polygon
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct PolygonData {
/// The radius of the polygon
pub radius: f64,
/// The number of sides in the polygon
pub num_sides: u64,
/// The center point of the polygon
pub center: [f64; 2],
/// The type of the polygon (inscribed or circumscribed)
#[serde(skip)]
polygon_type: PolygonType,
/// Whether the polygon is inscribed (true) or circumscribed (false) about a circle with the specified radius
#[serde(default = "default_inscribed")]
pub inscribed: bool,
}
fn default_inscribed() -> bool {
true
}
/// Create a regular polygon with the specified number of sides and radius.
pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch_surface_or_group, tag): (PolygonData, SketchOrSurface, Option<TagDeclarator>) =
args.get_polygon_args()?;
let sketch = inner_polygon(data, sketch_surface_or_group, tag, exec_state, args).await?;
Ok(KclValue::new_user_val(sketch.meta.clone(), sketch))
}
/// Create a regular polygon with the specified number of sides that is either inscribed or circumscribed around a circle of the specified radius.
///
/// ```no_run
/// // Create a regular hexagon inscribed in a circle of radius 10
/// hex = startSketchOn('XY')
/// |> polygon({
/// radius: 10,
/// numSides: 6,
/// center: [0, 0],
/// inscribed: true,
/// }, %)
///
/// example = extrude(5, hex)
/// ```
///
/// ```no_run
/// // Create a square circumscribed around a circle of radius 5
/// square = startSketchOn('XY')
/// |> polygon({
/// radius: 5.0,
/// numSides: 4,
/// center: [10, 10],
/// inscribed: false,
/// }, %)
/// example = extrude(5, square)
/// ```
#[stdlib {
name = "polygon",
}]
async fn inner_polygon(
data: PolygonData,
sketch_surface_or_group: SketchOrSurface,
tag: Option<TagDeclarator>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
if data.num_sides < 3 {
return Err(KclError::Type(KclErrorDetails {
message: "Polygon must have at least 3 sides".to_string(),
source_ranges: vec![args.source_range],
}));
}
if data.radius <= 0.0 {
return Err(KclError::Type(KclErrorDetails {
message: "Radius must be greater than 0".to_string(),
source_ranges: vec![args.source_range],
}));
}
let sketch_surface = match sketch_surface_or_group {
SketchOrSurface::SketchSurface(surface) => surface,
SketchOrSurface::Sketch(group) => group.on,
};
let half_angle = std::f64::consts::PI / data.num_sides as f64;
let radius_to_vertices = match data.polygon_type {
PolygonType::Inscribed => data.radius,
PolygonType::Circumscribed => data.radius / half_angle.cos(),
};
let angle_step = 2.0 * std::f64::consts::PI / data.num_sides as f64;
let vertices: Vec<[f64; 2]> = (0..data.num_sides)
.map(|i| {
let angle = angle_step * i as f64;
[
data.center[0] + radius_to_vertices * angle.cos(),
data.center[1] + radius_to_vertices * angle.sin(),
]
})
.collect();
let mut sketch =
crate::std::sketch::inner_start_profile_at(vertices[0], sketch_surface, None, exec_state, args.clone()).await?;
// Draw all the lines with unique IDs and modified tags
for vertex in vertices.iter().skip(1) {
let from = sketch.current_pen_position()?;
let id = exec_state.id_generator.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::ExtendPath {
path: sketch.id.into(),
segment: PathSegment::Line {
end: KPoint2d::from(*vertex).with_z(0.0).map(LengthUnit),
relative: false,
},
}),
)
.await?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to: *vertex,
tag: tag.clone(),
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),
},
},
};
if let Some(tag) = &tag {
sketch.add_tag(tag, &current_path);
}
sketch.paths.push(current_path);
}
// Close the polygon by connecting back to the first vertex with a new ID
let from = sketch.current_pen_position()?;
let close_id = exec_state.id_generator.next_uuid();
args.batch_modeling_cmd(
close_id,
ModelingCmd::from(mcmd::ExtendPath {
path: sketch.id.into(),
segment: PathSegment::Line {
end: KPoint2d::from(vertices[0]).with_z(0.0).map(LengthUnit),
relative: false,
},
}),
)
.await?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to: vertices[0],
tag: tag.clone(),
geo_meta: GeoMeta {
id: close_id,
metadata: args.source_range.into(),
},
},
};
if let Some(tag) = &tag {
sketch.add_tag(tag, &current_path);
}
sketch.paths.push(current_path);
args.batch_modeling_cmd(
exec_state.id_generator.next_uuid(),
ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
)
.await?;
Ok(sketch)
}

View File

@ -1501,7 +1501,7 @@ pub(crate) async fn inner_arc(
(center, a_start, a_end, *radius, end) (center, a_start, a_end, *radius, end)
} }
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, to.into(), center.into(), *radius, args.source_range)?;
(center.into(), angle_start, angle_end, *radius, to.into()) (center.into(), angle_start, angle_end, *radius, to.into())
} }
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,12 @@
arr = [1, 2, 3]
new_arr1 = push(arr, 4)
new_arr2 = push(new_arr1, 5)
assertEqual(new_arr1[0], 1, 0.00001, "element 0 should not have changed")
assertEqual(new_arr1[1], 2, 0.00001, "element 1 should not have changed")
assertEqual(new_arr1[2], 3, 0.00001, "element 2 should not have changed")
assertEqual(new_arr1[3], 4, 0.00001, "4 was added to the end of the array")
assertEqual(new_arr2[0], 1, 0.00001, "element 0 should not have changed")
assertEqual(new_arr2[1], 2, 0.00001, "element 1 should not have changed")
assertEqual(new_arr2[2], 3, 0.00001, "element 2 should not have changed")
assertEqual(new_arr2[3], 4, 0.00001, "4 was added to the end of the array")
assertEqual(new_arr2[4], 5, 0.00001, "5 was added to the end of the array")

View File

@ -173,3 +173,4 @@ gen_test_parse_fail!(
// ); // );
gen_test!(add_lots); gen_test!(add_lots);
gen_test!(double_map); gen_test!(double_map);
gen_test!(array_elem_push);