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>
@ -74,10 +74,12 @@ layout: manual
|
||||
* [`patternTransform`](kcl/patternTransform)
|
||||
* [`pi`](kcl/pi)
|
||||
* [`polar`](kcl/polar)
|
||||
* [`polygon`](kcl/polygon)
|
||||
* [`pow`](kcl/pow)
|
||||
* [`profileStart`](kcl/profileStart)
|
||||
* [`profileStartX`](kcl/profileStartX)
|
||||
* [`profileStartY`](kcl/profileStartY)
|
||||
* [`push`](kcl/push)
|
||||
* [`reduce`](kcl/reduce)
|
||||
* [`rem`](kcl/rem)
|
||||
* [`revolve`](kcl/revolve)
|
||||
|
60
docs/kcl/polygon.md
Normal file
38
docs/kcl/push.md
Normal file
16414
docs/kcl/std.json
24
docs/kcl/types/PolygonData.md
Normal 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 |
|
||||
|
||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
@ -100,6 +100,11 @@ export function Toolbar({
|
||||
function resolveItemConfig(
|
||||
maybeIconConfig: ToolbarItem
|
||||
): ToolbarItemResolved {
|
||||
const isDisabled =
|
||||
disableAllButtons ||
|
||||
maybeIconConfig.status !== 'available' ||
|
||||
maybeIconConfig.disabled?.(state) === true
|
||||
|
||||
return {
|
||||
...maybeIconConfig,
|
||||
title:
|
||||
@ -113,10 +118,11 @@ export function Toolbar({
|
||||
typeof maybeIconConfig.hotkey === 'string'
|
||||
? maybeIconConfig.hotkey
|
||||
: maybeIconConfig.hotkey?.(state),
|
||||
disabled:
|
||||
disableAllButtons ||
|
||||
maybeIconConfig.status !== 'available' ||
|
||||
maybeIconConfig.disabled?.(state) === true,
|
||||
disabled: isDisabled,
|
||||
disabledReason:
|
||||
typeof maybeIconConfig.disabledReason === 'function'
|
||||
? maybeIconConfig.disabledReason(state)
|
||||
: maybeIconConfig.disabledReason,
|
||||
disableHotkey: maybeIconConfig.disableHotkey?.(state),
|
||||
status: maybeIconConfig.status,
|
||||
}
|
||||
@ -273,6 +279,8 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
itemConfig: ToolbarItemResolved
|
||||
configCallbackProps: ToolbarItemCallbackProps
|
||||
}) {
|
||||
const { state } = useModelingContext()
|
||||
|
||||
useHotkeys(
|
||||
itemConfig.hotkey || '',
|
||||
() => {
|
||||
@ -336,6 +344,17 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
)}
|
||||
</div>
|
||||
<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 && (
|
||||
<>
|
||||
<hr className="border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
|
@ -88,8 +88,12 @@ export const MemoryPane = () => {
|
||||
export const processMemory = (programMemory: ProgramMemory) => {
|
||||
const processedMemory: any = {}
|
||||
for (const [key, val] of programMemory?.visibleEntries()) {
|
||||
if (typeof val.value !== 'function') {
|
||||
const sg = sketchFromKclValue(val, null)
|
||||
if (
|
||||
(val.type === 'UserVal' && val.value.type === 'Sketch') ||
|
||||
// @ts-ignore
|
||||
(val.type !== 'Function' && val.type !== 'UserVal')
|
||||
) {
|
||||
const sg = sketchFromKclValue(val, key)
|
||||
if (val.type === 'Solid') {
|
||||
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
|
||||
return rest
|
||||
@ -98,16 +102,17 @@ export const processMemory = (programMemory: ProgramMemory) => {
|
||||
processedMemory[key] = sg.paths.map(({ __geoMeta, ...rest }: Path) => {
|
||||
return rest
|
||||
})
|
||||
} else if ((val.type as any) === 'Function') {
|
||||
} else {
|
||||
processedMemory[key] = val.value
|
||||
}
|
||||
//@ts-ignore
|
||||
} else if (val.type === 'Function') {
|
||||
processedMemory[key] = `__function(${(val as any)?.expression?.params
|
||||
?.map?.(({ identifier }: any) => identifier?.name || '')
|
||||
.join(', ')})__`
|
||||
} else {
|
||||
processedMemory[key] = val.value
|
||||
}
|
||||
} else if (key !== 'log') {
|
||||
processedMemory[key] = '__function__'
|
||||
}
|
||||
}
|
||||
return processedMemory
|
||||
}
|
||||
|
@ -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 {
|
||||
Route,
|
||||
@ -13,7 +13,7 @@ import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||
type User = Models['User_type']
|
||||
|
||||
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 = {
|
||||
id: '8675309',
|
||||
name: 'Test User',
|
||||
@ -39,13 +39,19 @@ describe('UserSidebarMenu tests', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||
|
||||
await waitFor(() => {
|
||||
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 = {
|
||||
id: '8675309',
|
||||
email: 'kittycad.sidebar.test@example.com',
|
||||
@ -71,10 +77,12 @@ describe('UserSidebarMenu tests', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||
|
||||
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 = {
|
||||
id: '8675309',
|
||||
name: 'Test User',
|
||||
@ -98,10 +106,12 @@ describe('UserSidebarMenu tests', () => {
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent(
|
||||
'User menu'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
|
131
src/lib/desktop.test.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
})
|
@ -139,6 +139,11 @@ export async function listProjects(
|
||||
|
||||
const entries = await window.electron.readdir(projectDir)
|
||||
for (let entry of entries) {
|
||||
// Skip directories that start with a dot
|
||||
if (entry.startsWith('.')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const projectPath = window.electron.path.join(projectDir, entry)
|
||||
// if it's not a directory ignore.
|
||||
const isDirectory = await window.electron.statIsDirectory(projectPath)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs/promises'
|
||||
import { Stats } from 'fs'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
import { PROJECT_ENTRYPOINT } from './constants'
|
||||
|
||||
@ -43,8 +44,16 @@ export default async function getCurrentProjectFile(
|
||||
? 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.
|
||||
const stats = await fs.stat(sourcePath)
|
||||
if (stats.isDirectory()) {
|
||||
// Walk the directory and look for a kcl file.
|
||||
const files = await fs.readdir(sourcePath)
|
||||
|
@ -39,6 +39,9 @@ export type ToolbarItem = {
|
||||
description: string
|
||||
links: { label: string; url: string }[]
|
||||
isActive?: (state: StateFrom<typeof modelingMachine>) => boolean
|
||||
disabledReason?:
|
||||
| string
|
||||
| ((state: StateFrom<typeof modelingMachine>) => string | undefined)
|
||||
}
|
||||
|
||||
export type ToolbarItemResolved = Omit<
|
||||
@ -349,6 +352,11 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
(!isEditingExistingSketch(state.context) &&
|
||||
!state.matches({ Sketch: 'Tangential arc to' })) ||
|
||||
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',
|
||||
hotkey: (state) =>
|
||||
state.matches({ Sketch: 'Tangential arc to' }) ? ['Esc', 'A'] : 'A',
|
||||
|
@ -4,6 +4,7 @@ import { AppHeader } from 'components/AppHeader'
|
||||
import ProjectCard from 'components/ProjectCard/ProjectCard'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import Loading from 'components/Loading'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import {
|
||||
@ -94,6 +95,11 @@ const Home = () => {
|
||||
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) {
|
||||
send({
|
||||
type: 'Rename project',
|
||||
|
@ -1041,6 +1041,38 @@ impl LanguageServer for Backend {
|
||||
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(¤t_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());
|
||||
|
||||
// Add more to the completions if we have more.
|
||||
|
@ -3551,3 +3551,36 @@ const part001 = startSketchOn('XY')
|
||||
// Check the diagnostics.
|
||||
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);
|
||||
}
|
||||
|
@ -459,6 +459,19 @@ impl Args {
|
||||
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.
|
||||
@ -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::AngledLineThatIntersectsData);
|
||||
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::TangentialArcData);
|
||||
impl_from_arg_via_json!(super::sketch::BezierData);
|
||||
|
@ -193,3 +193,63 @@ async fn call_reduce_closure<'a>(
|
||||
})?;
|
||||
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
|
||||
}
|
||||
|
@ -66,6 +66,7 @@ lazy_static! {
|
||||
Box::new(crate::std::segment::AngleToMatchLengthX),
|
||||
Box::new(crate::std::segment::AngleToMatchLengthY),
|
||||
Box::new(crate::std::shapes::Circle),
|
||||
Box::new(crate::std::shapes::Polygon),
|
||||
Box::new(crate::std::sketch::LineTo),
|
||||
Box::new(crate::std::sketch::Line),
|
||||
Box::new(crate::std::sketch::XLineTo),
|
||||
@ -99,6 +100,7 @@ lazy_static! {
|
||||
Box::new(crate::std::patterns::PatternTransform),
|
||||
Box::new(crate::std::array::Reduce),
|
||||
Box::new(crate::std::array::Map),
|
||||
Box::new(crate::std::array::Push),
|
||||
Box::new(crate::std::chamfer::Chamfer),
|
||||
Box::new(crate::std::fillet::Fillet),
|
||||
Box::new(crate::std::fillet::GetOppositeEdge),
|
||||
|
@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
ast::types::TagDeclarator,
|
||||
errors::KclError,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{BasePath, ExecState, GeoMeta, KclValue, Path, Sketch, SketchSurface},
|
||||
std::Args,
|
||||
};
|
||||
@ -24,6 +24,7 @@ use crate::{
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(untagged)]
|
||||
|
||||
pub enum SketchOrSurface {
|
||||
SketchSurface(SketchSurface),
|
||||
Sketch(Box<Sketch>),
|
||||
@ -141,3 +142,201 @@ async fn inner_circle(
|
||||
|
||||
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, ¤t_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, ¤t_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)
|
||||
}
|
||||
|
@ -1501,7 +1501,7 @@ pub(crate) async fn inner_arc(
|
||||
(center, a_start, a_end, *radius, end)
|
||||
}
|
||||
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())
|
||||
}
|
||||
};
|
||||
|
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_polygon0.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_polygon1.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_push0.png
Normal file
After Width: | Height: | Size: 19 KiB |
@ -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")
|
@ -173,3 +173,4 @@ gen_test_parse_fail!(
|
||||
// );
|
||||
gen_test!(add_lots);
|
||||
gen_test!(double_map);
|
||||
gen_test!(array_elem_push);
|
||||
|