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)
|
* [`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
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(
|
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" />
|
||||||
|
@ -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
|
||||||
|
@ -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
@ -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)
|
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)
|
||||||
|
@ -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)
|
||||||
|
@ -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',
|
||||||
|
@ -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',
|
||||||
|
@ -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(¤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());
|
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.
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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, ¤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)
|
(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())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
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!(add_lots);
|
||||||
gen_test!(double_map);
|
gen_test!(double_map);
|
||||||
|
gen_test!(array_elem_push);
|
||||||
|