Compare commits

...

5 Commits

Author SHA1 Message Date
07eaf93e78 Cut release v0.21.1 (#2347) 2024-05-13 10:05:29 -04:00
6a5ca3088a remove getExtrudeWallTransform (#2342)
* remove getExtrudeWallTransform

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

* docs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-05-10 17:28:14 -07:00
6501072d80 turn on formatting test now working (#2341)
turn on test now working

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-05-10 23:51:54 +00:00
726fd02bad Add a component for toolbar buttons with a dropdown, consolidate Constrain actions under one (#2327) 2024-05-10 19:02:11 -04:00
d0f9ae475f format button wasnt working, will add playwright test so we don't regress again (#2340)
* add test

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

* add another test

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

* add another test

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

* updates

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

* off by one error

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

* add two tests

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

* fix typos

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

* updates

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

* updates

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

* updates

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

* fix semantic tokens for commants

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

* fix tests

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-05-10 22:30:40 +00:00
216 changed files with 790 additions and 1300 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -31,7 +31,6 @@ layout: manual
* [`fillet`](kcl/fillet) * [`fillet`](kcl/fillet)
* [`floor`](kcl/floor) * [`floor`](kcl/floor)
* [`getEdge`](kcl/getEdge) * [`getEdge`](kcl/getEdge)
* [`getExtrudeWallTransform`](kcl/getExtrudeWallTransform)
* [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge) * [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge)
* [`getOppositeEdge`](kcl/getOppositeEdge) * [`getOppositeEdge`](kcl/getOppositeEdge)
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -28338,839 +28338,6 @@
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 10], %)\n |> line([10, 0], %)\n |> line([0, -10], %, 'revolveAxis')\n |> close(%)\n |> extrude(10, %)\n\nconst sketch001 = startSketchOn('XY')\n |> startProfileAt([0, -10], %)\n |> line([0, -10], %)\n |> line([2, 0], %)\n |> line([0, 10], %)\n |> close(%)\n |> revolve({\n axis: getEdge('revolveAxis', box),\n angle: 90\n }, %)" "const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 10], %)\n |> line([10, 0], %)\n |> line([0, -10], %, 'revolveAxis')\n |> close(%)\n |> extrude(10, %)\n\nconst sketch001 = startSketchOn('XY')\n |> startProfileAt([0, -10], %)\n |> line([0, -10], %)\n |> line([2, 0], %)\n |> line([0, 10], %)\n |> close(%)\n |> revolve({\n axis: getEdge('revolveAxis', box),\n angle: 90\n }, %)"
] ]
}, },
{
"name": "getExtrudeWallTransform",
"summary": "Returns the extrude wall transform.",
"description": "",
"tags": [],
"args": [
{
"name": "surface_name",
"type": "string",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "extrude_group",
"type": "ExtrudeGroup",
"schema": {
"description": "An extrude group is a collection of extrude surfaces.",
"type": "object",
"required": [
"__meta",
"height",
"id",
"position",
"rotation",
"sketchGroupValues",
"value",
"xAxis",
"yAxis",
"zAxis"
],
"properties": {
"__meta": {
"description": "Metadata.",
"type": "array",
"items": {
"description": "Metadata.",
"type": "object",
"required": [
"sourceRange"
],
"properties": {
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
}
}
}
},
"endCapId": {
"description": "The id of the extrusion end cap",
"type": "string",
"format": "uuid",
"nullable": true
},
"height": {
"description": "The height of the extrude group.",
"type": "number",
"format": "double"
},
"id": {
"description": "The id of the extrude group.",
"type": "string",
"format": "uuid"
},
"position": {
"description": "The position of the extrude group.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 3,
"minItems": 3
},
"rotation": {
"description": "The rotation of the extrude group.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 4,
"minItems": 4
},
"sketchGroupValues": {
"description": "The sketch group paths.",
"type": "array",
"items": {
"description": "A path.",
"oneOf": [
{
"description": "A path that goes to a point.",
"type": "object",
"required": [
"__geoMeta",
"from",
"name",
"to",
"type"
],
"properties": {
"__geoMeta": {
"description": "Metadata.",
"type": "object",
"required": [
"id",
"sourceRange"
],
"properties": {
"id": {
"description": "The id of the geometry.",
"type": "string",
"format": "uuid"
},
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
}
}
},
"from": {
"description": "The from point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"name": {
"description": "The name of the path.",
"type": "string"
},
"to": {
"description": "The to point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"type": {
"type": "string",
"enum": [
"ToPoint"
]
}
}
},
{
"description": "A arc that is tangential to the last path segment that goes to a point",
"type": "object",
"required": [
"__geoMeta",
"ccw",
"center",
"from",
"name",
"to",
"type"
],
"properties": {
"__geoMeta": {
"description": "Metadata.",
"type": "object",
"required": [
"id",
"sourceRange"
],
"properties": {
"id": {
"description": "The id of the geometry.",
"type": "string",
"format": "uuid"
},
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
}
}
},
"ccw": {
"description": "arc's direction",
"type": "boolean"
},
"center": {
"description": "the arc's center",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"from": {
"description": "The from point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"name": {
"description": "The name of the path.",
"type": "string"
},
"to": {
"description": "The to point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"type": {
"type": "string",
"enum": [
"TangentialArcTo"
]
}
}
},
{
"description": "A arc that is tangential to the last path segment",
"type": "object",
"required": [
"__geoMeta",
"from",
"name",
"to",
"type"
],
"properties": {
"__geoMeta": {
"description": "Metadata.",
"type": "object",
"required": [
"id",
"sourceRange"
],
"properties": {
"id": {
"description": "The id of the geometry.",
"type": "string",
"format": "uuid"
},
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
}
}
},
"from": {
"description": "The from point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"name": {
"description": "The name of the path.",
"type": "string"
},
"to": {
"description": "The to point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"type": {
"type": "string",
"enum": [
"TangentialArc"
]
}
}
},
{
"description": "A path that is horizontal.",
"type": "object",
"required": [
"__geoMeta",
"from",
"name",
"to",
"type",
"x"
],
"properties": {
"__geoMeta": {
"description": "Metadata.",
"type": "object",
"required": [
"id",
"sourceRange"
],
"properties": {
"id": {
"description": "The id of the geometry.",
"type": "string",
"format": "uuid"
},
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
}
}
},
"from": {
"description": "The from point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"name": {
"description": "The name of the path.",
"type": "string"
},
"to": {
"description": "The to point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"type": {
"type": "string",
"enum": [
"Horizontal"
]
},
"x": {
"description": "The x coordinate.",
"type": "number",
"format": "double"
}
}
},
{
"description": "An angled line to.",
"type": "object",
"required": [
"__geoMeta",
"from",
"name",
"to",
"type"
],
"properties": {
"__geoMeta": {
"description": "Metadata.",
"type": "object",
"required": [
"id",
"sourceRange"
],
"properties": {
"id": {
"description": "The id of the geometry.",
"type": "string",
"format": "uuid"
},
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
}
}
},
"from": {
"description": "The from point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"name": {
"description": "The name of the path.",
"type": "string"
},
"to": {
"description": "The to point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"type": {
"type": "string",
"enum": [
"AngledLineTo"
]
},
"x": {
"description": "The x coordinate.",
"type": "number",
"format": "double",
"nullable": true
},
"y": {
"description": "The y coordinate.",
"type": "number",
"format": "double",
"nullable": true
}
}
},
{
"description": "A base path.",
"type": "object",
"required": [
"__geoMeta",
"from",
"name",
"to",
"type"
],
"properties": {
"__geoMeta": {
"description": "Metadata.",
"type": "object",
"required": [
"id",
"sourceRange"
],
"properties": {
"id": {
"description": "The id of the geometry.",
"type": "string",
"format": "uuid"
},
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
}
}
},
"from": {
"description": "The from point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"name": {
"description": "The name of the path.",
"type": "string"
},
"to": {
"description": "The to point.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
},
"type": {
"type": "string",
"enum": [
"Base"
]
}
}
}
]
}
},
"startCapId": {
"description": "The id of the extrusion start cap",
"type": "string",
"format": "uuid",
"nullable": true
},
"value": {
"description": "The extrude surfaces.",
"type": "array",
"items": {
"description": "An extrude surface.",
"oneOf": [
{
"description": "An extrude plane.",
"type": "object",
"required": [
"faceId",
"id",
"name",
"position",
"rotation",
"sourceRange",
"type"
],
"properties": {
"faceId": {
"description": "The face id for the extrude plane.",
"type": "string",
"format": "uuid"
},
"id": {
"description": "The id of the geometry.",
"type": "string",
"format": "uuid"
},
"name": {
"description": "The name.",
"type": "string"
},
"position": {
"description": "The position.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 3,
"minItems": 3
},
"rotation": {
"description": "The rotation.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 4,
"minItems": 4
},
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
},
"type": {
"type": "string",
"enum": [
"extrudePlane"
]
}
}
},
{
"description": "An extruded arc.",
"type": "object",
"required": [
"faceId",
"id",
"name",
"position",
"rotation",
"sourceRange",
"type"
],
"properties": {
"faceId": {
"description": "The face id for the extrude plane.",
"type": "string",
"format": "uuid"
},
"id": {
"description": "The id of the geometry.",
"type": "string",
"format": "uuid"
},
"name": {
"description": "The name.",
"type": "string"
},
"position": {
"description": "The position.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 3,
"minItems": 3
},
"rotation": {
"description": "The rotation.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 4,
"minItems": 4
},
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
},
"type": {
"type": "string",
"enum": [
"extrudeArc"
]
}
}
}
]
}
},
"xAxis": {
"description": "The x-axis of the extrude group base plane in the 3D space",
"type": "object",
"required": [
"x",
"y",
"z"
],
"properties": {
"x": {
"type": "number",
"format": "double"
},
"y": {
"type": "number",
"format": "double"
},
"z": {
"type": "number",
"format": "double"
}
}
},
"yAxis": {
"description": "The y-axis of the extrude group base plane in the 3D space",
"type": "object",
"required": [
"x",
"y",
"z"
],
"properties": {
"x": {
"type": "number",
"format": "double"
},
"y": {
"type": "number",
"format": "double"
},
"z": {
"type": "number",
"format": "double"
}
}
},
"zAxis": {
"description": "The z-axis of the extrude group base plane in the 3D space",
"type": "object",
"required": [
"x",
"y",
"z"
],
"properties": {
"x": {
"type": "number",
"format": "double"
},
"y": {
"type": "number",
"format": "double"
},
"z": {
"type": "number",
"format": "double"
}
}
}
}
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "ExtrudeTransform",
"schema": {
"type": "object",
"required": [
"__meta",
"position",
"rotation"
],
"properties": {
"__meta": {
"type": "array",
"items": {
"description": "Metadata.",
"type": "object",
"required": [
"sourceRange"
],
"properties": {
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
}
}
}
},
"position": {
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 3,
"minItems": 3
},
"rotation": {
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 4,
"minItems": 4
}
}
},
"required": true
},
"unpublished": false,
"deprecated": false,
"examples": [
"const box = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([0, 10], %)\n |> line([10, 0], %)\n |> line([0, -10], %, \"surface\")\n |> close(%)\n |> extrude(5, %)\n\nconst transform = getExtrudeWallTransform('surface', box)"
]
},
{ {
"name": "getNextAdjacentEdge", "name": "getNextAdjacentEdge",
"summary": "Get the next adjacent edge to the edge given.", "summary": "Get the next adjacent edge to the edge given.",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -131,6 +131,7 @@ test('Basic sketch', async ({ page }) => {
// selected two lines therefore there should be two cursors // selected two lines therefore there should be two cursors
await expect(page.locator('.cm-cursor')).toHaveCount(2) await expect(page.locator('.cm-cursor')).toHaveCount(2)
await page.getByRole('button', { name: 'Constrain' }).click()
await page.getByRole('button', { name: 'Equal Length' }).click() await page.getByRole('button', { name: 'Equal Length' }).click()
await expect(page.locator('.cm-content')) await expect(page.locator('.cm-content'))
@ -264,6 +265,88 @@ test('Can moving camera', async ({ page, context }) => {
}, [1, -94, -94]) }, [1, -94, -94])
}) })
test('if you click the format button it formats your code', async ({
page,
}) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await page.click('.cm-content')
await page.keyboard.type(`const part001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
await page.click('#code-pane button:first-child')
await page.click('button:has-text("Format code")')
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
})
test('if you use the format keyboard binding it formats your code', async ({
page,
}) => {
const u = getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
const lspStartPromise = page.waitForEvent('console', async (message) => {
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
// but that doesn't seem to make it to the console for macos/safari :(
if (message.text().includes('start kcl lsp')) {
await new Promise((resolve) => setTimeout(resolve, 200))
return true
}
return false
})
await page.goto('/')
await u.waitForAuthSkipAppStart()
await lspStartPromise
// check no error to begin with
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// focus the editor
await page.click('.cm-line')
// Hit alt+shift+f to format the code
await page.keyboard.press('Alt+Shift+KeyF')
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
})
test('if you write invalid kcl you get inlined errors', async ({ page }) => { test('if you write invalid kcl you get inlined errors', async ({ page }) => {
const u = getUtils(page) const u = getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
@ -529,10 +612,6 @@ test.describe('Can create sketches on all planes and their back sides', () => {
}) })
test('Auto complete works', async ({ page }) => { test('Auto complete works', async ({ page }) => {
test.skip(
true,
'CORS issue stopping the kcl lsp from working, enable again later'
)
const u = getUtils(page) const u = getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio // const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -862,11 +941,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// click a segment hold shift and click an axis, see that a relevant constraint is enabled // click a segment hold shift and click an axis, see that a relevant constraint is enabled
await topHorzSegmentClick() await topHorzSegmentClick()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
const constrainButton = page.getByRole('button', { name: 'Constrain' })
const absYButton = page.getByRole('button', { name: 'ABS Y' }) const absYButton = page.getByRole('button', { name: 'ABS Y' })
await constrainButton.click()
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await xAxisClick() await xAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await constrainButton.click()
await absYButton.and(page.locator(':not([disabled])')).waitFor() await absYButton.and(page.locator(':not([disabled])')).waitFor()
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
@ -876,12 +958,14 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await page.waitForTimeout(100) await page.waitForTimeout(100)
// same selection but click the axis first // same selection but click the axis first
await xAxisClick() await xAxisClick()
await constrainButton.click()
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.waitForTimeout(100) await page.waitForTimeout(100)
await topHorzSegmentClick() await topHorzSegmentClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing // clear selection by clicking on nothing
@ -890,10 +974,12 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// check the same selection again by putting cursor in code first then selecting axis // check the same selection again by putting cursor in code first then selecting axis
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click() await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await constrainButton.click()
await expect(absYButton).toBeDisabled() await expect(absYButton).toBeDisabled()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await xAxisClick() await xAxisClick()
await page.keyboard.up('Shift') await page.keyboard.up('Shift')
await constrainButton.click()
await expect(absYButton).not.toBeDisabled() await expect(absYButton).not.toBeDisabled()
// clear selection by clicking on nothing // clear selection by clicking on nothing
@ -1827,6 +1913,7 @@ test('Can code mod a line length', async ({ page }) => {
const startXPx = 500 const startXPx = 500
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10) await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.mouse.click(615, 102) await page.mouse.click(615, 102)
await page.getByRole('button', { name: 'Constrain', exact: true }).click()
await page.getByRole('button', { name: 'length', exact: true }).click() await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByText('Add constraining value').click() await page.getByText('Add constraining value').click()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.21.0", "version": "0.21.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.0",

View File

@ -74,5 +74,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.21.0" "version": "0.21.1"
} }

View File

@ -11,17 +11,18 @@ import {
useNetworkStatus, useNetworkStatus,
} from 'components/NetworkHealthIndicator' } from 'components/NetworkHealthIndicator'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
export const Toolbar = () => { export const Toolbar = () => {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const toolbarButtonsRef = useRef<HTMLUListElement>(null) const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const iconClassName = const iconClassName =
'group-disabled:text-chalkboard-50 group-enabled:group-hover:!text-chalkboard-10 group-pressed:!text-chalkboard-10' 'group-disabled:text-chalkboard-50 group-enabled:group-hover:!text-primary dark:group-enabled:group-hover:!text-inherit group-pressed:!text-chalkboard-10 group-ui-open:!text-chalkboard-10 dark:group-ui-open:!text-chalkboard-10'
const bgClassName = const bgClassName =
'group-disabled:!bg-transparent group-enabled:group-hover:bg-primary group-pressed:bg-primary' 'group-disabled:!bg-transparent group-enabled:group-hover:bg-primary/10 dark:group-enabled:group-hover:bg-primary group-pressed:bg-primary group-ui-open:bg-primary'
const buttonClassName = const buttonClassName =
'bg-chalkboard-10 dark:bg-chalkboard-100 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100' 'bg-chalkboard-10 dark:bg-chalkboard-100 enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!border-primary ui-open:!border-primary'
const pathId = useMemo(() => { const pathId = useMemo(() => {
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) { if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast)) {
return false return false
@ -57,10 +58,7 @@ export const Toolbar = () => {
{...props} {...props}
ref={toolbarButtonsRef} ref={toolbarButtonsRef}
onWheel={handleToolbarButtonsWheelEvent} onWheel={handleToolbarButtonsWheelEvent}
className={ className={'m-0 py-1 rounded-l-sm flex gap-2 items-center ' + className}
'm-0 py-1 rounded-l-sm flex gap-2 items-center overflow-x-auto ' +
className
}
style={{ scrollbarWidth: 'thin' }} style={{ scrollbarWidth: 'thin' }}
> >
{state.nextEvents.includes('Enter sketch') && ( {state.nextEvents.includes('Enter sketch') && (
@ -71,7 +69,7 @@ export const Toolbar = () => {
onClick={() => onClick={() =>
send({ type: 'Enter sketch', data: { forceNewSketch: true } }) send({ type: 'Enter sketch', data: { forceNewSketch: true } })
} }
icon={{ iconStart={{
icon: 'sketch', icon: 'sketch',
iconClassName, iconClassName,
bgClassName, bgClassName,
@ -88,7 +86,7 @@ export const Toolbar = () => {
className={buttonClassName} className={buttonClassName}
Element="button" Element="button"
onClick={() => send({ type: 'Enter sketch' })} onClick={() => send({ type: 'Enter sketch' })}
icon={{ iconStart={{
icon: 'sketch', icon: 'sketch',
iconClassName, iconClassName,
bgClassName, bgClassName,
@ -105,7 +103,7 @@ export const Toolbar = () => {
className={buttonClassName} className={buttonClassName}
Element="button" Element="button"
onClick={() => send({ type: 'Cancel' })} onClick={() => send({ type: 'Cancel' })}
icon={{ iconStart={{
icon: 'arrowLeft', icon: 'arrowLeft',
iconClassName, iconClassName,
bgClassName, bgClassName,
@ -128,7 +126,7 @@ export const Toolbar = () => {
: send('Equip Line tool') : send('Equip Line tool')
} }
aria-pressed={state?.matches('Sketch.Line tool')} aria-pressed={state?.matches('Sketch.Line tool')}
icon={{ iconStart={{
icon: 'line', icon: 'line',
iconClassName, iconClassName,
bgClassName, bgClassName,
@ -148,7 +146,7 @@ export const Toolbar = () => {
: send('Equip tangential arc to') : send('Equip tangential arc to')
} }
aria-pressed={state.matches('Sketch.Tangential arc to')} aria-pressed={state.matches('Sketch.Tangential arc to')}
icon={{ iconStart={{
icon: 'arc', icon: 'arc',
iconClassName, iconClassName,
bgClassName, bgClassName,
@ -172,7 +170,7 @@ export const Toolbar = () => {
: send('Equip rectangle tool') : send('Equip rectangle tool')
} }
aria-pressed={state.matches('Sketch.Rectangle tool')} aria-pressed={state.matches('Sketch.Rectangle tool')}
icon={{ iconStart={{
icon: 'rectangle', icon: 'rectangle',
iconClassName, iconClassName,
bgClassName, bgClassName,
@ -194,52 +192,54 @@ export const Toolbar = () => {
</> </>
)} )}
{state.matches('Sketch.SketchIdle') && {state.matches('Sketch.SketchIdle') &&
state.nextEvents state.nextEvents.filter(
.filter( (eventName) =>
(eventName) => eventName.includes('Make segment') ||
eventName.includes('Make segment') || eventName.includes('Constrain')
eventName.includes('Constrain') ).length > 0 && (
) <ActionButtonDropdown
.sort((a, b) => { splitMenuItems={state.nextEvents
const aisEnabled = state.nextEvents .filter(
.filter((event) => state.can(event as any)) (eventName) =>
.includes(a) eventName.includes('Make segment') ||
const bIsEnabled = state.nextEvents eventName.includes('Constrain')
.filter((event) => state.can(event as any)) )
.includes(b) .sort((a, b) => {
if (aisEnabled && !bIsEnabled) { const aisEnabled = state.nextEvents
return -1 .filter((event) => state.can(event as any))
} .includes(a)
if (!aisEnabled && bIsEnabled) { const bIsEnabled = state.nextEvents
return 1 .filter((event) => state.can(event as any))
} .includes(b)
return 0 if (aisEnabled && !bIsEnabled) {
}) return -1
.map((eventName) => ( }
<li className="contents" key={eventName}> if (!aisEnabled && bIsEnabled) {
<ActionButton return 1
className={buttonClassName} }
Element="button" return 0
key={eventName} })
onClick={() => send(eventName)} .map((eventName) => ({
disabled={ label: eventName
.replace('Make segment ', '')
.replace('Constrain ', ''),
onClick: () => send(eventName),
disabled:
!state.nextEvents !state.nextEvents
.filter((event) => state.can(event as any)) .filter((event) => state.can(event as any))
.includes(eventName) || disableAllButtons .includes(eventName) || disableAllButtons,
} }))}
title={eventName} className={buttonClassName}
icon={{ Element="button"
icon: 'line', iconStart={{
iconClassName, icon: 'dimension',
bgClassName, iconClassName,
}} bgClassName,
> }}
{eventName >
.replace('Make segment ', '') Constrain
.replace('Constrain ', '')} </ActionButtonDropdown>
</ActionButton> )}
</li>
))}
{state.matches('idle') && ( {state.matches('idle') && (
<li className="contents"> <li className="contents">
<ActionButton <ActionButton
@ -257,7 +257,7 @@ export const Toolbar = () => {
? 'extrude' ? 'extrude'
: 'sketches need to be closed, or not already extruded' : 'sketches need to be closed, or not already extruded'
} }
icon={{ iconStart={{
icon: 'extrude', icon: 'extrude',
iconClassName, iconClassName,
bgClassName, bgClassName,
@ -272,7 +272,7 @@ export const Toolbar = () => {
} }
return ( return (
<menu className="max-w-full overflow-hidden whitespace-nowrap rounded px-1.5 py-0.5 backdrop-blur-sm bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative"> <menu className="max-w-full whitespace-nowrap rounded px-1.5 py-0.5 backdrop-blur-sm bg-chalkboard-10/80 dark:bg-chalkboard-110/70 relative">
<ToolbarButtons /> <ToolbarButtons />
</menu> </menu>
) )

View File

@ -1,11 +1,12 @@
import { ActionIcon, ActionIconProps } from './ActionIcon' import { ActionIcon, ActionIconProps } from './ActionIcon'
import React from 'react' import React, { ForwardedRef, forwardRef } from 'react'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import type { LinkProps } from 'react-router-dom' import type { LinkProps } from 'react-router-dom'
interface BaseActionButtonProps { interface BaseActionButtonProps {
icon?: ActionIconProps iconStart?: ActionIconProps
iconEnd?: ActionIconProps
className?: string className?: string
} }
@ -32,15 +33,15 @@ type ActionButtonAsElement = BaseActionButtonProps &
Element: React.ComponentType<React.HTMLAttributes<HTMLButtonElement>> Element: React.ComponentType<React.HTMLAttributes<HTMLButtonElement>>
} }
type ActionButtonProps = export type ActionButtonProps =
| ActionButtonAsButton | ActionButtonAsButton
| ActionButtonAsLink | ActionButtonAsLink
| ActionButtonAsExternal | ActionButtonAsExternal
| ActionButtonAsElement | ActionButtonAsElement
export const ActionButton = (props: ActionButtonProps) => { export const ActionButton = forwardRef((props: ActionButtonProps, ref) => {
const classNames = `action-button m-0 group mono text-sm flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 p-[3px] text-chalkboard-100 dark:text-chalkboard-10 ${ const classNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10 ${
props.icon ? 'pr-2' : 'px-2' props.iconStart ? (props.iconEnd ? 'px-0' : 'pr-2') : 'px-2'
} ${props.className ? props.className : ''}` } ${props.className ? props.className : ''}`
switch (props.Element) { switch (props.Element) {
@ -48,11 +49,23 @@ export const ActionButton = (props: ActionButtonProps) => {
// Note we have to destructure 'className' and 'Element' out of props // Note we have to destructure 'className' and 'Element' out of props
// because we don't want to pass them to the button element; // because we don't want to pass them to the button element;
// the same is true for the other cases below. // the same is true for the other cases below.
const { Element, icon, children, className: _className, ...rest } = props const {
Element,
iconStart,
iconEnd,
children,
className: _className,
...rest
} = props
return ( return (
<button className={classNames} {...rest}> <button
{props.icon && <ActionIcon {...icon} />} ref={ref as ForwardedRef<HTMLButtonElement>}
className={classNames}
{...rest}
>
{iconStart && <ActionIcon {...iconStart} />}
{children} {children}
{iconEnd && <ActionIcon {...iconEnd} />}
</button> </button>
) )
} }
@ -60,15 +73,22 @@ export const ActionButton = (props: ActionButtonProps) => {
const { const {
Element, Element,
to, to,
icon, iconStart,
iconEnd,
children, children,
className: _className, className: _className,
...rest ...rest
} = props } = props
return ( return (
<Link to={to || paths.INDEX} className={classNames} {...rest}> <Link
{icon && <ActionIcon {...icon} />} ref={ref as ForwardedRef<HTMLAnchorElement>}
to={to || paths.INDEX}
className={classNames}
{...rest}
>
{iconStart && <ActionIcon {...iconStart} />}
{children} {children}
{iconEnd && <ActionIcon {...iconEnd} />}
</Link> </Link>
) )
} }
@ -76,33 +96,42 @@ export const ActionButton = (props: ActionButtonProps) => {
const { const {
Element, Element,
to, to,
icon, iconStart,
iconEnd,
children, children,
className: _className, className: _className,
...rest ...rest
} = props } = props
return ( return (
<Link <Link
ref={ref as ForwardedRef<HTMLAnchorElement>}
to={to || paths.INDEX} to={to || paths.INDEX}
className={classNames} className={classNames}
{...rest} {...rest}
target="_blank" target="_blank"
> >
{icon && <ActionIcon {...icon} />} {iconStart && <ActionIcon {...iconStart} />}
{children} {children}
{iconEnd && <ActionIcon {...iconEnd} />}
</Link> </Link>
) )
} }
default: { default: {
const { Element, icon, children, className: _className, ...rest } = props const {
if (!Element) throw new Error('Element is required') Element,
iconStart,
children,
className: _className,
...rest
} = props
return ( return (
<Element className={classNames} {...rest}> <Element className={classNames} {...rest}>
{props.icon && <ActionIcon {...props.icon} />} {props.iconStart && <ActionIcon {...props.iconStart} />}
{children} {children}
{props.iconEnd && <ActionIcon {...props.iconEnd} />}
</Element> </Element>
) )
} }
} }
} })

View File

@ -0,0 +1,55 @@
import { Popover } from '@headlessui/react'
import { ActionButton, ActionButtonProps } from './ActionButton'
type ActionButtonSplitProps = Omit<ActionButtonProps, 'iconEnd'> & {
splitMenuItems: {
label: string
shortcut?: string
onClick: () => void
disabled?: boolean
}[]
}
export function ActionButtonDropdown({
splitMenuItems,
className,
...props
}: ActionButtonSplitProps) {
return (
<Popover className="relative">
<Popover.Button
as={ActionButton}
className={className}
{...props}
Element="button"
iconEnd={{
icon: 'caretDown',
className: 'ui-open:rotate-180',
bgClassName:
'bg-chalkboard-20 dark:bg-chalkboard-80 ui-open:bg-primary ui-open:text-chalkboard-10',
}}
/>
<Popover.Panel
as="ul"
className="absolute z-20 left-1/2 -translate-x-1/2 top-full mt-1 w-fit max-h-[80vh] overflow-y-auto py-2 flex flex-col gap-1 align-stretch text-inherit dark:text-chalkboard-10 bg-chalkboard-10 dark:bg-chalkboard-100 rounded shadow-lg border border-solid border-chalkboard-30 dark:border-chalkboard-80 text-sm m-0 p-0"
>
{splitMenuItems.map((item) => (
<li className="contents" key={item.label}>
<button
onClick={item.onClick}
className="block px-3 py-1 hover:bg-primary/10 dark:hover:bg-chalkboard-80 border-0 m-0 text-sm w-full rounded-none text-left disabled:!bg-transparent dark:disabled:text-chalkboard-60"
disabled={item.disabled}
>
<span className="capitalize">{item.label}</span>
{item.shortcut && (
<kbd className="bg-primary/10 dark:bg-chalkboard-80 dark:group-hover:bg-primary font-mono rounded-sm dark:text-inherit inline-block px-1 border-primary dark:border-chalkboard-90">
{item.shortcut}
</kbd>
)}
</button>
</li>
))}
</Popover.Panel>
</Popover>
)
}

View File

@ -29,10 +29,8 @@ export const ActionIcon = ({
size = 'md', size = 'md',
children, children,
}: ActionIconProps) => { }: ActionIconProps) => {
// By default, we reverse the icon color and background color in dark mode const computedIconClassName = `h-auto text-inherit dark:text-current !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}`
const computedIconClassName = `h-auto text-primary dark:text-current !group-disabled:text-chalkboard-60 !group-disabled:text-chalkboard-60 ${iconClassName}` const computedBgClassName = `bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
const computedBgClassName = `bg-primary/10 dark:bg-chalkboard-90 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 ${bgClassName}`
return ( return (
<div <div

View File

@ -175,7 +175,7 @@ function ReviewingButton() {
type="submit" type="submit"
form="review-form" form="review-form"
className="w-fit !p-0 rounded-sm border !border-primary hover:shadow" className="w-fit !p-0 rounded-sm border !border-primary hover:shadow"
icon={{ iconStart={{
icon: 'checkmark', icon: 'checkmark',
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110', bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',
iconClassName: '!text-chalkboard-10', iconClassName: '!text-chalkboard-10',
@ -193,7 +193,7 @@ function GatheringArgsButton() {
type="submit" type="submit"
form="arg-form" form="arg-form"
className="w-fit !p-0 rounded-sm border !border-primary hover:shadow" className="w-fit !p-0 rounded-sm border !border-primary hover:shadow"
icon={{ iconStart={{
icon: 'arrowRight', icon: 'arrowRight',
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110', bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',
iconClassName: '!text-chalkboard-10', iconClassName: '!text-chalkboard-10',

View File

@ -71,6 +71,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
caretDown: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 11.2929L6.35346 7.64642L5.64636 8.35354L9.64648 12.3536L10.3536 12.3536L14.3535 8.35353L13.6464 7.64643L10 11.2929Z"
fill="currentColor"
/>
</svg>
),
clipboardCheckmark: ( clipboardCheckmark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
@ -101,6 +111,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
dimension: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.6455 3.6455L14 2V5.291L5.291 14H2L6 18V14.7052L14.7052 6H18L16.3526 4.35261L16.3546 4.35065L15.6475 3.64354L15.6455 3.6455Z"
fill="currentColor"
/>
</svg>
),
equal: ( equal: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@ -25,7 +25,7 @@ const DownloadAppBanner = () => {
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => setIsBannerDismissed(true)} onClick={() => setIsBannerDismissed(true)}
icon={{ iconStart={{
icon: 'close', icon: 'close',
className: 'p-1', className: 'p-1',
bgClassName: bgClassName:

View File

@ -29,7 +29,7 @@ export const ErrorPage = () => {
<ActionButton <ActionButton
Element="link" Element="link"
to={'/'} to={'/'}
icon={{ icon: faHome }} iconStart={{ icon: faHome }}
data-testid="unexpected-error-home" data-testid="unexpected-error-home"
> >
Go Home Go Home
@ -37,14 +37,14 @@ export const ErrorPage = () => {
)} )}
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ icon: faRefresh }} iconStart={{ icon: faRefresh }}
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
> >
Reload Reload
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ icon: faTrash }} iconStart={{ icon: faTrash }}
onClick={() => { onClick={() => {
window.localStorage.clear() window.localStorage.clear()
}} }}
@ -53,7 +53,7 @@ export const ErrorPage = () => {
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
icon={{ icon: faBug }} iconStart={{ icon: faBug }}
to="https://github.com/KittyCAD/modeling-app/issues/new" to="https://github.com/KittyCAD/modeling-app/issues/new"
> >
Report Bug Report Bug

View File

@ -109,7 +109,7 @@ function DeleteConfirmationDialog({
send({ type: 'Delete file', data: fileOrDir }) send({ type: 'Delete file', data: fileOrDir })
setIsOpen(false) setIsOpen(false)
}} }}
icon={{ iconStart={{
icon: faTrashAlt, icon: faTrashAlt,
bgClassName: 'bg-destroy-80', bgClassName: 'bg-destroy-80',
iconClassName: iconClassName:
@ -340,7 +340,7 @@ export const FileTreeMenu = () => {
<> <>
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ iconStart={{
icon: 'filePlus', icon: 'filePlus',
iconClassName: '!text-current', iconClassName: '!text-current',
bgClassName: 'bg-transparent', bgClassName: 'bg-transparent',
@ -355,7 +355,7 @@ export const FileTreeMenu = () => {
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ iconStart={{
icon: 'folderPlus', icon: 'folderPlus',
iconClassName: '!text-current', iconClassName: '!text-current',
bgClassName: 'bg-transparent', bgClassName: 'bg-transparent',

View File

@ -85,7 +85,7 @@ function ProjectCard({
<ActionButton <ActionButton
Element="button" Element="button"
type="submit" type="submit"
icon={{ iconStart={{
icon: faCheck, icon: faCheck,
size: 'sm', size: 'sm',
className: 'p-1', className: 'p-1',
@ -99,7 +99,7 @@ function ProjectCard({
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ iconStart={{
icon: faX, icon: faX,
size: 'sm', size: 'sm',
iconClassName: 'dark:!text-chalkboard-20', iconClassName: 'dark:!text-chalkboard-20',
@ -141,7 +141,7 @@ function ProjectCard({
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"> <div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ iconStart={{
icon: faPenAlt, icon: faPenAlt,
className: 'p-1', className: 'p-1',
iconClassName: 'dark:!text-chalkboard-20', iconClassName: 'dark:!text-chalkboard-20',
@ -161,7 +161,7 @@ function ProjectCard({
</ActionButton> </ActionButton>
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ iconStart={{
icon: faTrashAlt, icon: faTrashAlt,
className: 'p-1', className: 'p-1',
size: 'xs', size: 'xs',
@ -207,7 +207,7 @@ function ProjectCard({
await handleDeleteProject(project) await handleDeleteProject(project)
setIsConfirmingDelete(false) setIsConfirmingDelete(false)
}} }}
icon={{ iconStart={{
icon: faTrashAlt, icon: faTrashAlt,
bgClassName: 'bg-destroy-80', bgClassName: 'bg-destroy-80',
className: 'p-1', className: 'p-1',

View File

@ -165,7 +165,7 @@ function ProjectMenuPopover({
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90"> <div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ icon: 'exportFile', className: 'p-1' }} iconStart={{ icon: 'exportFile', className: 'p-1' }}
className="border-transparent dark:border-transparent" className="border-transparent dark:border-transparent"
disabled={!findCommand(exportCommandInfo)} disabled={!findCommand(exportCommandInfo)}
onClick={() => onClick={() =>
@ -185,7 +185,7 @@ function ProjectMenuPopover({
// Clear the scene and end the session. // Clear the scene and end the session.
engineCommandManager.endSession() engineCommandManager.endSession()
}} }}
icon={{ iconStart={{
icon: 'arrowLeft', icon: 'arrowLeft',
className: 'p-1', className: 'p-1',
}} }}

View File

@ -78,7 +78,7 @@ export const SetVarNameModal = ({
Element="button" Element="button"
type="submit" type="submit"
disabled={!isNewVariableNameUnique} disabled={!isNewVariableNameUnique}
icon={{ icon: faPlus }} iconStart={{ icon: faPlus }}
> >
Add variable Add variable
</ActionButton> </ActionButton>

View File

@ -59,7 +59,7 @@ export const UpdaterModal = ({
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => onResolve({ wantUpdate: false })} onClick={() => onResolve({ wantUpdate: false })}
icon={{ iconStart={{
icon: 'close', icon: 'close',
bgClassName: 'bg-destroy-80', bgClassName: 'bg-destroy-80',
iconClassName: 'text-destroy-20 group-hover:text-destroy-10', iconClassName: 'text-destroy-20 group-hover:text-destroy-10',
@ -72,7 +72,10 @@ export const UpdaterModal = ({
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => onResolve({ wantUpdate: true })} onClick={() => onResolve({ wantUpdate: true })}
icon={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }} iconStart={{
icon: 'arrowRight',
bgClassName: 'dark:bg-chalkboard-80',
}}
className="dark:hover:bg-chalkboard-80/50" className="dark:hover:bg-chalkboard-80/50"
data-testid="update-button-update" data-testid="update-button-update"
> >

View File

@ -31,7 +31,7 @@ export const UpdaterRestartModal = ({
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => onResolve({ wantRestart: false })} onClick={() => onResolve({ wantRestart: false })}
icon={{ iconStart={{
icon: 'close', icon: 'close',
bgClassName: 'bg-destroy-80', bgClassName: 'bg-destroy-80',
iconClassName: 'text-destroy-20 group-hover:text-destroy-10', iconClassName: 'text-destroy-20 group-hover:text-destroy-10',
@ -44,7 +44,10 @@ export const UpdaterRestartModal = ({
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => onResolve({ wantRestart: true })} onClick={() => onResolve({ wantRestart: true })}
icon={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }} iconStart={{
icon: 'arrowRight',
bgClassName: 'dark:bg-chalkboard-80',
}}
className="dark:hover:bg-chalkboard-80/50" className="dark:hover:bg-chalkboard-80/50"
data-testid="update-restrart-button-update" data-testid="update-restrart-button-update"
> >

View File

@ -55,7 +55,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
) : ( ) : (
<ActionButton <ActionButton
Element={Popover.Button} Element={Popover.Button}
icon={{ icon: 'menu' }} iconStart={{ icon: 'menu' }}
className="border-transparent !px-0" className="border-transparent !px-0"
data-testid="user-sidebar-toggle" data-testid="user-sidebar-toggle"
> >
@ -120,7 +120,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
<div className="p-4 flex flex-col gap-2"> <div className="p-4 flex flex-col gap-2">
<ActionButton <ActionButton
Element="button" Element="button"
icon={{ icon: 'settings' }} iconStart={{ icon: 'settings' }}
className="border-transparent dark:border-transparent hover:bg-transparent" className="border-transparent dark:border-transparent hover:bg-transparent"
onClick={() => { onClick={() => {
// since /settings is a nested route the sidebar doesn't close // since /settings is a nested route the sidebar doesn't close
@ -138,7 +138,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://github.com/KittyCAD/modeling-app/discussions" to="https://github.com/KittyCAD/modeling-app/discussions"
icon={{ icon: faGithub, className: 'p-1', size: 'sm' }} iconStart={{ icon: faGithub, className: 'p-1', size: 'sm' }}
className="border-transparent dark:border-transparent" className="border-transparent dark:border-transparent"
> >
Request a feature Request a feature
@ -146,7 +146,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
<ActionButton <ActionButton
Element="externalLink" Element="externalLink"
to="https://github.com/KittyCAD/modeling-app/issues/new/choose" to="https://github.com/KittyCAD/modeling-app/issues/new/choose"
icon={{ icon: faBug, className: 'p-1', size: 'sm' }} iconStart={{ icon: faBug, className: 'p-1', size: 'sm' }}
className="border-transparent dark:border-transparent" className="border-transparent dark:border-transparent"
> >
Report a bug Report a bug
@ -154,7 +154,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => send('Log out')} onClick={() => send('Log out')}
icon={{ iconStart={{
icon: faSignOutAlt, icon: faSignOutAlt,
className: 'p-1', className: 'p-1',
bgClassName: '!bg-transparent', bgClassName: '!bg-transparent',

View File

@ -24,7 +24,7 @@ export function WasmErrBanner() {
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => setBannerDismissed(true)} onClick={() => setBannerDismissed(true)}
icon={{ iconStart={{
icon: 'close', icon: 'close',
className: 'p-1', className: 'p-1',
bgClassName: bgClassName:

View File

@ -264,8 +264,12 @@ export class LanguageServerPlugin implements PluginValue {
) )
return null return null
this.sendChange({ this.client.textDocumentDidChange({
documentText: this.view.state.doc.toString(), textDocument: {
uri: this.documentUri,
version: this.documentVersion++,
},
contentChanges: [{ text: this.view.state.doc.toString() }],
}) })
const result = await this.client.textDocumentFormatting({ const result = await this.client.textDocumentFormatting({

View File

@ -79,6 +79,7 @@ export default class CodeManager {
if (this._code !== code) { if (this._code !== code) {
this.code = code this.code = code
this.#updateState(code) this.#updateState(code)
this.updateCodeEditor(code)
} }
} }

View File

@ -210,7 +210,7 @@ const Home = () => {
: '') : '')
} }
onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))} onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
icon={{ iconStart={{
icon: getSortIcon(sort, 'name'), icon: getSortIcon(sort, 'name'),
className: 'p-1.5', className: 'p-1.5',
iconClassName: !sort.includes('name') iconClassName: !sort.includes('name')
@ -232,7 +232,7 @@ const Home = () => {
onClick={() => onClick={() =>
setSearchParams(getNextSearchParams(sort, 'modified')) setSearchParams(getNextSearchParams(sort, 'modified'))
} }
icon={{ iconStart={{
icon: sort ? getSortIcon(sort, 'modified') : faArrowDown, icon: sort ? getSortIcon(sort, 'modified') : faArrowDown,
className: 'p-1.5', className: 'p-1.5',
iconClassName: !isSortByModified ? '!text-chalkboard-40' : '', iconClassName: !isSortByModified ? '!text-chalkboard-40' : '',
@ -278,7 +278,7 @@ const Home = () => {
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => send('Create project')} onClick={() => send('Create project')}
icon={{ icon: faPlus, iconClassName: 'p-1 w-4' }} iconStart={{ icon: faPlus, iconClassName: 'p-1 w-4' }}
data-testid="home-new-file" data-testid="home-new-file"
> >
New project New project

View File

@ -51,7 +51,7 @@ export default function Units() {
<ActionButton <ActionButton
Element="button" Element="button"
onClick={dismiss} onClick={dismiss}
icon={{ iconStart={{
icon: faXmark, icon: faXmark,
bgClassName: 'bg-destroy-80', bgClassName: 'bg-destroy-80',
iconClassName: iconClassName:
@ -64,7 +64,7 @@ export default function Units() {
<ActionButton <ActionButton
Element="button" Element="button"
onClick={next} onClick={next}
icon={{ icon: faArrowRight }} iconStart={{ icon: faArrowRight }}
> >
Next: Camera Next: Camera
</ActionButton> </ActionButton>

View File

@ -144,7 +144,7 @@ export function OnboardingButtons({
<ActionButton <ActionButton
Element="button" Element="button"
onClick={dismiss} onClick={dismiss}
icon={{ iconStart={{
icon: 'close', icon: 'close',
bgClassName: 'bg-destroy-80', bgClassName: 'bg-destroy-80',
iconClassName: 'text-destroy-20 group-hover:text-destroy-10', iconClassName: 'text-destroy-20 group-hover:text-destroy-10',
@ -161,7 +161,7 @@ export function OnboardingButtons({
<ActionButton <ActionButton
Element="button" Element="button"
onClick={next} onClick={next}
icon={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }} iconStart={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }}
className="dark:hover:bg-chalkboard-80/50" className="dark:hover:bg-chalkboard-80/50"
data-testid="onboarding-next" data-testid="onboarding-next"
> >

View File

@ -269,7 +269,7 @@ export const Settings = () => {
<ActionButton <ActionButton
Element="button" Element="button"
onClick={restartOnboarding} onClick={restartOnboarding}
icon={{ iconStart={{
icon: 'refresh', icon: 'refresh',
size: 'sm', size: 'sm',
className: 'p-1', className: 'p-1',
@ -300,7 +300,7 @@ export const Settings = () => {
) )
showInFolder(paths[searchParamTab]) showInFolder(paths[searchParamTab])
}} }}
icon={{ iconStart={{
icon: 'folder', icon: 'folder',
size: 'sm', size: 'sm',
className: 'p-1', className: 'p-1',
@ -319,7 +319,7 @@ export const Settings = () => {
}) })
toast.success('Settings restored to default') toast.success('Settings restored to default')
}} }}
icon={{ iconStart={{
icon: 'refresh', icon: 'refresh',
size: 'sm', size: 'sm',
className: 'p-1 text-chalkboard-10', className: 'p-1 text-chalkboard-10',

View File

@ -65,7 +65,7 @@ const SignIn = () => {
<ActionButton <ActionButton
Element="button" Element="button"
onClick={signInTauri} onClick={signInTauri}
icon={{ icon: 'arrowRight' }} iconStart={{ icon: 'arrowRight' }}
className="w-fit mt-4" className="w-fit mt-4"
data-testid="sign-in-button" data-testid="sign-in-button"
> >
@ -80,7 +80,7 @@ const SignIn = () => {
typeof window !== 'undefined' && typeof window !== 'undefined' &&
window.location.href.replace('signin', '') window.location.href.replace('signin', '')
)}`} )}`}
icon={{ icon: 'arrowRight' }} iconStart={{ icon: 'arrowRight' }}
className="w-fit mt-4" className="w-fit mt-4"
> >
Sign in Sign in

View File

@ -1895,7 +1895,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.54" version = "0.1.55"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.1.54" version = "0.1.55"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -3297,6 +3297,144 @@ fn ghi = (x) => {
assert_eq!(symbols.len(), 7); assert_eq!(symbols.len(), 7);
} }
#[test]
fn test_recast_bug_extra_parens() {
let some_program_string = r#"// Ball Bearing
// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads.
// Define constants like ball diameter, inside diameter, overhange length, and thickness
const sphereDia = 0.5
const insideDia = 1
const thickness = 0.25
const overHangLength = .4
// Sketch and revolve the inside bearing piece
const insideRevolve = startSketchOn('XZ')
|> startProfileAt([insideDia / 2, 0], %)
|> line([0, thickness + sphereDia / 2], %)
|> line([overHangLength, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, -sphereDia], %)
|> line([overHangLength - thickness, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength, 0], %)
|> close(%)
|> revolve({ axis: 'y' }, %)
// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
const sphere = startSketchOn('XZ')
|> startProfileAt([
0.05 + insideDia / 2 + thickness,
0 - 0.05
], %)
|> line([sphereDia - 0.1, 0], %)
|> arc({
angle_start: 0,
angle_end: -180,
radius: sphereDia / 2 - 0.05
}, %)
|> close(%)
|> revolve({ axis: 'x' }, %)
|> patternCircular3d({
axis: [0, 0, 1],
center: [0, 0, 0],
repetitions: 10,
arcDegrees: 360,
rotateDuplicates: true
}, %)
// Sketch and revolve the outside bearing
const outsideRevolve = startSketchOn('XZ')
|> startProfileAt([
insideDia / 2 + thickness + sphereDia,
0
], %)
|> line([0, sphereDia / 2], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, thickness], %)
|> line([overHangLength, 0], %)
|> line([0, -2 * thickness - sphereDia], %)
|> line([-overHangLength, 0], %)
|> line([0, thickness], %)
|> line([overHangLength - thickness, 0], %)
|> close(%)
|> revolve({ axis: 'y' }, %)"#;
let tokens = crate::token::lexer(some_program_string).unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
println!("{:#?}", program);
let recasted = program.recast(&Default::default(), 0);
assert_eq!(
recasted,
r#"// Ball Bearing
// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads.
// Define constants like ball diameter, inside diameter, overhange length, and thickness
const sphereDia = 0.5
const insideDia = 1
const thickness = 0.25
const overHangLength = .4
// Sketch and revolve the inside bearing piece
const insideRevolve = startSketchOn('XZ')
|> startProfileAt([insideDia / 2, 0], %)
|> line([0, thickness + sphereDia / 2], %)
|> line([overHangLength, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, -sphereDia], %)
|> line([overHangLength - thickness, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength, 0], %)
|> close(%)
|> revolve({ axis: 'y' }, %)
// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
const sphere = startSketchOn('XZ')
|> startProfileAt([
0.05 + insideDia / 2 + thickness,
0 - 0.05
], %)
|> line([sphereDia - 0.1, 0], %)
|> arc({
angle_start: 0,
angle_end: -180,
radius: sphereDia / 2 - 0.05
}, %)
|> close(%)
|> revolve({ axis: 'x' }, %)
|> patternCircular3d({
axis: [0, 0, 1],
center: [0, 0, 0],
repetitions: 10,
arcDegrees: 360,
rotateDuplicates: true
}, %)
// Sketch and revolve the outside bearing
const outsideRevolve = startSketchOn('XZ')
|> startProfileAt([
insideDia / 2 + thickness + sphereDia,
0
], %)
|> line([0, sphereDia / 2], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, thickness], %)
|> line([overHangLength, 0], %)
|> line([0, -2 * thickness - sphereDia], %)
|> line([-overHangLength, 0], %)
|> line([0, thickness], %)
|> line([overHangLength - thickness, 0], %)
|> close(%)
|> revolve({ axis: 'y' }, %)
"#
);
}
#[test] #[test]
fn test_recast_empty_file() { fn test_recast_empty_file() {
let some_program_string = r#""#; let some_program_string = r#""#;

View File

@ -144,8 +144,6 @@ pub enum MemoryItem {
}, },
ImportedGeometry(ImportedGeometry), ImportedGeometry(ImportedGeometry),
#[ts(skip)] #[ts(skip)]
ExtrudeTransform(Box<ExtrudeTransform>),
#[ts(skip)]
Function { Function {
#[serde(skip)] #[serde(skip)]
func: Option<MemoryFunction>, func: Option<MemoryFunction>,
@ -300,16 +298,6 @@ pub struct UserVal {
pub meta: Vec<Metadata>, pub meta: Vec<Metadata>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct ExtrudeTransform {
pub position: Position,
pub rotation: Rotation,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
pub type MemoryFunction = pub type MemoryFunction =
fn( fn(
s: Vec<MemoryItem>, s: Vec<MemoryItem>,
@ -348,7 +336,6 @@ impl From<MemoryItem> for Vec<SourceRange> {
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range)) .flat_map(|eg| eg.meta.iter().map(|m| m.source_range))
.collect(), .collect(),
MemoryItem::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(), MemoryItem::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(), MemoryItem::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
MemoryItem::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(), MemoryItem::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::Face(f) => f.meta.iter().map(|m| m.source_range).collect(), MemoryItem::Face(f) => f.meta.iter().map(|m| m.source_range).collect(),

View File

@ -313,6 +313,28 @@ impl Backend {
let source_range: SourceRange = token.clone().into(); let source_range: SourceRange = token.clone().into();
let position = source_range.start_to_lsp_position(&params.text); let position = source_range.start_to_lsp_position(&params.text);
// We need to check if we are on the last token of the line.
// If we are starting from the end of the last line just add 1 to the line.
// Check if we are on the last token of the line.
if let Some(line) = params.text.lines().nth(position.line as usize) {
if line.len() == position.character as usize {
// We are on the last token of the line.
// We need to add a new line.
let semantic_token = SemanticToken {
delta_line: position.line - last_position.line + 1,
delta_start: 0,
length: token.value.len() as u32,
token_type: token_type_index as u32,
token_modifiers_bitset: 0,
};
semantic_tokens.push(semantic_token);
last_position = Position::new(position.line + 1, 0);
continue;
}
}
let semantic_token = SemanticToken { let semantic_token = SemanticToken {
delta_line: position.line - last_position.line, delta_line: position.line - last_position.line,
delta_start: if position.line != last_position.line { delta_start: if position.line != last_position.line {
@ -1011,7 +1033,7 @@ impl LanguageServer for Backend {
}, },
0, 0,
); );
let source_range = SourceRange([0, current_code.len() - 1]); let source_range = SourceRange([0, current_code.len()]);
let range = source_range.to_lsp_range(current_code); let range = source_range.to_lsp_range(current_code);
Ok(Some(vec![TextEdit { Ok(Some(vec![TextEdit {
new_text: recast, new_text: recast,

View File

@ -1014,15 +1014,86 @@ async fn test_kcl_lsp_semantic_tokens() {
if let tower_lsp::lsp_types::SemanticTokensResult::Tokens(semantic_tokens) = semantic_tokens { if let tower_lsp::lsp_types::SemanticTokensResult::Tokens(semantic_tokens) = semantic_tokens {
assert_eq!(semantic_tokens.data.len(), 2); assert_eq!(semantic_tokens.data.len(), 2);
assert_eq!(semantic_tokens.data[0].length, 13); assert_eq!(semantic_tokens.data[0].length, 13);
assert_eq!(semantic_tokens.data[0].delta_start, 0);
assert_eq!(semantic_tokens.data[0].delta_line, 0);
assert_eq!(semantic_tokens.data[0].token_type, 8); assert_eq!(semantic_tokens.data[0].token_type, 8);
assert_eq!(semantic_tokens.data[1].length, 4); assert_eq!(semantic_tokens.data[1].length, 4);
assert_eq!(semantic_tokens.data[1].delta_start, 14); assert_eq!(semantic_tokens.data[1].delta_start, 14);
assert_eq!(semantic_tokens.data[1].delta_line, 0);
assert_eq!(semantic_tokens.data[1].token_type, 3); assert_eq!(semantic_tokens.data[1].token_type, 3);
} else { } else {
panic!("Expected semantic tokens"); panic!("Expected semantic tokens");
} }
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_semantic_tokens_multiple_comments() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
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: r#"// Ball Bearing
// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads.
// Define constants like ball diameter, inside diameter, overhange length, and thickness
const sphereDia = 0.5"#.to_string(),
},
})
.await;
server.wait_on_handle().await;
// Send semantic tokens request.
let semantic_tokens = server
.semantic_tokens_full(tower_lsp::lsp_types::SemanticTokensParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
})
.await
.unwrap()
.unwrap();
// Check the semantic tokens.
if let tower_lsp::lsp_types::SemanticTokensResult::Tokens(semantic_tokens) = semantic_tokens {
assert_eq!(semantic_tokens.data.len(), 7);
assert_eq!(semantic_tokens.data[0].length, 15);
assert_eq!(semantic_tokens.data[0].delta_start, 0);
assert_eq!(semantic_tokens.data[0].delta_line, 0);
assert_eq!(semantic_tokens.data[0].token_type, 6);
assert_eq!(semantic_tokens.data[1].length, 232);
assert_eq!(semantic_tokens.data[1].delta_start, 0);
assert_eq!(semantic_tokens.data[1].delta_line, 1);
assert_eq!(semantic_tokens.data[1].token_type, 6);
assert_eq!(semantic_tokens.data[2].length, 88);
assert_eq!(semantic_tokens.data[2].delta_start, 0);
assert_eq!(semantic_tokens.data[2].delta_line, 2);
assert_eq!(semantic_tokens.data[2].token_type, 6);
assert_eq!(semantic_tokens.data[3].length, 5);
assert_eq!(semantic_tokens.data[3].delta_start, 0);
assert_eq!(semantic_tokens.data[3].delta_line, 1);
assert_eq!(semantic_tokens.data[3].token_type, 4);
assert_eq!(semantic_tokens.data[4].length, 9);
assert_eq!(semantic_tokens.data[4].delta_start, 6);
assert_eq!(semantic_tokens.data[4].delta_line, 0);
assert_eq!(semantic_tokens.data[4].token_type, 1);
assert_eq!(semantic_tokens.data[5].length, 1);
assert_eq!(semantic_tokens.data[5].delta_start, 10);
assert_eq!(semantic_tokens.data[5].token_type, 2);
assert_eq!(semantic_tokens.data[6].length, 3);
assert_eq!(semantic_tokens.data[6].delta_start, 2);
assert_eq!(semantic_tokens.data[6].token_type, 0);
} else {
panic!("Expected semantic tokens");
}
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_document_symbol() { async fn test_kcl_lsp_document_symbol() {
let server = kcl_lsp_server(false).await.unwrap(); let server = kcl_lsp_server(false).await.unwrap();
@ -1112,6 +1183,183 @@ async fn test_kcl_lsp_formatting() {
); );
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_formatting_extra_parens() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
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: r#"// Ball Bearing
// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads.
// Define constants like ball diameter, inside diameter, overhange length, and thickness
const sphereDia = 0.5
const insideDia = 1
const thickness = 0.25
const overHangLength = .4
// Sketch and revolve the inside bearing piece
const insideRevolve = startSketchOn('XZ')
|> startProfileAt([insideDia / 2, 0], %)
|> line([0, thickness + sphereDia / 2], %)
|> line([overHangLength, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, -sphereDia], %)
|> line([overHangLength - thickness, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength, 0], %)
|> close(%)
|> revolve({ axis: 'y' }, %)
// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
const sphere = startSketchOn('XZ')
|> startProfileAt([
0.05 + insideDia / 2 + thickness,
0 - 0.05
], %)
|> line([sphereDia - 0.1, 0], %)
|> arc({
angle_start: 0,
angle_end: -180,
radius: sphereDia / 2 - 0.05
}, %)
|> close(%)
|> revolve({ axis: 'x' }, %)
|> patternCircular3d({
axis: [0, 0, 1],
center: [0, 0, 0],
repetitions: 10,
arcDegrees: 360,
rotateDuplicates: true
}, %)
// Sketch and revolve the outside bearing
const outsideRevolve = startSketchOn('XZ')
|> startProfileAt([
insideDia / 2 + thickness + sphereDia,
0
], %)
|> line([0, sphereDia / 2], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, thickness], %)
|> line([overHangLength, 0], %)
|> line([0, -2 * thickness - sphereDia], %)
|> line([-overHangLength, 0], %)
|> line([0, thickness], %)
|> line([overHangLength - thickness, 0], %)
|> close(%)
|> revolve({ axis: 'y' }, %)"#
.to_string(),
},
})
.await;
server.wait_on_handle().await;
// Send formatting request.
let formatting = server
.formatting(tower_lsp::lsp_types::DocumentFormattingParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
options: tower_lsp::lsp_types::FormattingOptions {
tab_size: 2,
insert_spaces: true,
properties: Default::default(),
trim_trailing_whitespace: None,
insert_final_newline: None,
trim_final_newlines: None,
},
work_done_progress_params: Default::default(),
})
.await
.unwrap()
.unwrap();
// Check the formatting.
assert_eq!(formatting.len(), 1);
assert_eq!(
formatting[0].range,
tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 0, character: 0 },
end: tower_lsp::lsp_types::Position {
line: 60,
character: 30
}
}
);
assert_eq!(
formatting[0].new_text,
r#"// Ball Bearing
// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads.
// Define constants like ball diameter, inside diameter, overhange length, and thickness
const sphereDia = 0.5
const insideDia = 1
const thickness = 0.25
const overHangLength = .4
// Sketch and revolve the inside bearing piece
const insideRevolve = startSketchOn('XZ')
|> startProfileAt([insideDia / 2, 0], %)
|> line([0, thickness + sphereDia / 2], %)
|> line([overHangLength, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, -sphereDia], %)
|> line([overHangLength - thickness, 0], %)
|> line([0, -thickness], %)
|> line([-overHangLength, 0], %)
|> close(%)
|> revolve({ axis: 'y' }, %)
// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
const sphere = startSketchOn('XZ')
|> startProfileAt([
0.05 + insideDia / 2 + thickness,
0 - 0.05
], %)
|> line([sphereDia - 0.1, 0], %)
|> arc({
angle_start: 0,
angle_end: -180,
radius: sphereDia / 2 - 0.05
}, %)
|> close(%)
|> revolve({ axis: 'x' }, %)
|> patternCircular3d({
axis: [0, 0, 1],
center: [0, 0, 0],
repetitions: 10,
arcDegrees: 360,
rotateDuplicates: true
}, %)
// Sketch and revolve the outside bearing
const outsideRevolve = startSketchOn('XZ')
|> startProfileAt([
insideDia / 2 + thickness + sphereDia,
0
], %)
|> line([0, sphereDia / 2], %)
|> line([-overHangLength + thickness, 0], %)
|> line([0, thickness], %)
|> line([overHangLength, 0], %)
|> line([0, -2 * thickness - sphereDia], %)
|> line([-overHangLength, 0], %)
|> line([0, thickness], %)
|> line([overHangLength - thickness, 0], %)
|> close(%)
|> revolve({ axis: 'y' }, %)"#
);
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_rename() { async fn test_kcl_lsp_rename() {
let server = kcl_lsp_server(false).await.unwrap(); let server = kcl_lsp_server(false).await.unwrap();

View File

@ -8,8 +8,8 @@ use uuid::Uuid;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ executor::{
ExtrudeGroup, ExtrudeGroupSet, ExtrudeSurface, ExtrudeTransform, GeoMeta, MemoryItem, Path, SketchGroup, ExtrudeGroup, ExtrudeGroupSet, ExtrudeSurface, GeoMeta, MemoryItem, Path, SketchGroup, SketchGroupSet,
SketchGroupSet, SketchSurface, SketchSurface,
}, },
std::Args, std::Args,
}; };
@ -224,48 +224,3 @@ pub(crate) async fn do_post_extrude(
meta: sketch_group.meta, meta: sketch_group.meta,
})) }))
} }
/// Returns the extrude wall transform.
pub async fn get_extrude_wall_transform(args: Args) -> Result<MemoryItem, KclError> {
let (surface_name, extrude_group) = args.get_path_name_extrude_group()?;
let result = inner_get_extrude_wall_transform(&surface_name, *extrude_group, args)?;
Ok(MemoryItem::ExtrudeTransform(result))
}
/// Returns the extrude wall transform.
///
/// ```no_run
/// const box = startSketchOn('XY')
/// |> startProfileAt([0, 0], %)
/// |> line([0, 10], %)
/// |> line([10, 0], %)
/// |> line([0, -10], %, "surface")
/// |> close(%)
/// |> extrude(5, %)
///
/// const transform = getExtrudeWallTransform('surface', box)
/// ```
#[stdlib {
name = "getExtrudeWallTransform"
}]
fn inner_get_extrude_wall_transform(
surface_name: &str,
extrude_group: ExtrudeGroup,
args: Args,
) -> Result<Box<ExtrudeTransform>, KclError> {
let surface = extrude_group.get_path_by_name(surface_name).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a surface name that exists in the given ExtrudeGroup, found `{}`",
surface_name
),
source_ranges: vec![args.source_range],
})
})?;
Ok(Box::new(ExtrudeTransform {
position: surface.get_position(),
rotation: surface.get_rotation(),
meta: extrude_group.meta,
}))
}

View File

@ -44,7 +44,6 @@ lazy_static! {
Box::new(LegAngX), Box::new(LegAngX),
Box::new(LegAngY), Box::new(LegAngY),
Box::new(crate::std::extrude::Extrude), Box::new(crate::std::extrude::Extrude),
Box::new(crate::std::extrude::GetExtrudeWallTransform),
Box::new(crate::std::segment::SegEndX), Box::new(crate::std::segment::SegEndX),
Box::new(crate::std::segment::SegEndY), Box::new(crate::std::segment::SegEndY),
Box::new(crate::std::segment::LastSegX), Box::new(crate::std::segment::LastSegX),
@ -906,54 +905,6 @@ impl Args {
Ok((number, sketch_set)) Ok((number, sketch_set))
} }
fn get_path_name_extrude_group(&self) -> Result<(String, Box<ExtrudeGroup>), KclError> {
// Iterate over our args, the first argument should be a UserVal with a string value.
// The second argument should be a ExtrudeGroup.
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let path_name = if let serde_json::Value::String(s) = first_value {
s.to_string()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a string as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
}));
};
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!(
"Expected a ExtrudeGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range],
})
})?;
let extrude_group = if let MemoryItem::ExtrudeGroup(sg) = second_value {
sg.clone()
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a ExtrudeGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range],
}));
};
Ok((path_name, extrude_group))
}
} }
/// Returns the length of the given leg. /// Returns the length of the given leg.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Some files were not shown because too many files have changed in this diff Show More