Compare commits
108 Commits
cut-releas
...
stream-pau
Author | SHA1 | Date | |
---|---|---|---|
c6b80eec68 | |||
1b68f5dc19 | |||
a0aa4802d1 | |||
746f76ec63 | |||
8e624935c2 | |||
0a5f22c80a | |||
142db64796 | |||
59b0cdc3ac | |||
4763257dc3 | |||
b48ba7f081 | |||
64537a59b8 | |||
c24efaf2e4 | |||
2fb16ed074 | |||
82e647db3b | |||
fa2d0a69bf | |||
e372b2680e | |||
c1c1f817c9 | |||
00c0c993f2 | |||
b505c0be07 | |||
6c2d06c2c6 | |||
a4b7dd5182 | |||
0c2ca726d0 | |||
fcfecf702b | |||
347a6ef15a | |||
eed4386f76 | |||
14afcba599 | |||
faee6cbc64 | |||
0673e98fad | |||
b4eea5f842 | |||
2c9eb7f7c0 | |||
e259b2e3e8 | |||
91049204c5 | |||
0128c67aae | |||
ecc42b1e9c | |||
31811d0269 | |||
def5959836 | |||
4c7fab405b | |||
1e12e8d36b | |||
ad775891a3 | |||
efe207f4d2 | |||
01f0162991 | |||
2bbf7fad67 | |||
98549945a4 | |||
315fdc3060 | |||
c7e77e2597 | |||
a48679c014 | |||
c5e74866a9 | |||
731cb6c532 | |||
8a36a4c205 | |||
f29f2557de | |||
5f0ffb56c4 | |||
70078176b0 | |||
098fa2b5c9 | |||
2755156b84 | |||
a8b3ec660d | |||
3747c6ff0e | |||
bc1bc817ba | |||
b415e88746 | |||
9173e368a2 | |||
25928813e3 | |||
aec9cac7c7 | |||
0e82fbf7b0 | |||
f5975bbd61 | |||
765e587f6b | |||
6ccd5e22b8 | |||
c8bf82ba04 | |||
daad2039ec | |||
b567f6dfad | |||
41e85c77ac | |||
9f615b9d3e | |||
e8b5618b34 | |||
799b2d77b4 | |||
7b569f9b4f | |||
0f0c396a0c | |||
83214a88a3 | |||
dbab7876de | |||
6706695502 | |||
fa1f8d8d02 | |||
b4e59b5c56 | |||
20495383ac | |||
7f5fb83761 | |||
2ac874971e | |||
230e3132e9 | |||
bf9bb4fb22 | |||
31e7634669 | |||
84c71aa046 | |||
721b3e8cbd | |||
89309b6ccd | |||
15b163bba8 | |||
60d047ef6a | |||
f105044a47 | |||
9388e09c47 | |||
d71f2af9bd | |||
2bb372de12 | |||
0a3a8afbbd | |||
351df2f306 | |||
05a2eada9a | |||
788270d4fc | |||
6845f0c4bc | |||
563096fba4 | |||
35133c4f45 | |||
b78c6508c2 | |||
08b776134f | |||
cca544189c | |||
69754c82a2 | |||
afbee552ee | |||
b11772b27c | |||
6dc87aa4fe |
@ -7,7 +7,6 @@ layout: manual
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
* [Types](kcl/types)
|
* [Types](kcl/types)
|
||||||
* [Modules](kcl/modules)
|
|
||||||
* [Known Issues](kcl/KNOWN-ISSUES)
|
* [Known Issues](kcl/KNOWN-ISSUES)
|
||||||
* [`abs`](kcl/abs)
|
* [`abs`](kcl/abs)
|
||||||
* [`acos`](kcl/acos)
|
* [`acos`](kcl/acos)
|
||||||
@ -20,7 +19,6 @@ layout: manual
|
|||||||
* [`angledLineToX`](kcl/angledLineToX)
|
* [`angledLineToX`](kcl/angledLineToX)
|
||||||
* [`angledLineToY`](kcl/angledLineToY)
|
* [`angledLineToY`](kcl/angledLineToY)
|
||||||
* [`arc`](kcl/arc)
|
* [`arc`](kcl/arc)
|
||||||
* [`arcTo`](kcl/arcTo)
|
|
||||||
* [`asin`](kcl/asin)
|
* [`asin`](kcl/asin)
|
||||||
* [`assert`](kcl/assert)
|
* [`assert`](kcl/assert)
|
||||||
* [`assertEqual`](kcl/assertEqual)
|
* [`assertEqual`](kcl/assertEqual)
|
||||||
|
@ -9,7 +9,7 @@ Offset a plane by a distance along its normal.
|
|||||||
For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ' plane and 10 units away from it.
|
For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ' plane and 10 units away from it.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
offsetPlane(std_plane: StandardPlane, offset: number) -> Plane
|
offsetPlane(std_plane: StandardPlane, offset: number) -> PlaneData
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ offsetPlane(std_plane: StandardPlane, offset: number) -> Plane
|
|||||||
|
|
||||||
### Returns
|
### Returns
|
||||||
|
|
||||||
[`Plane`](/docs/kcl/types/Plane) - A plane.
|
[`PlaneData`](/docs/kcl/types/PlaneData) - Data for a plane.
|
||||||
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
4616
docs/kcl/std.json
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
title: "ArcToData"
|
|
||||||
excerpt: "Data to draw a three point arc (arcTo)."
|
|
||||||
layout: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
Data to draw a three point arc (arcTo).
|
|
||||||
|
|
||||||
**Type:** `object`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Properties
|
|
||||||
|
|
||||||
| Property | Type | Description | Required |
|
|
||||||
|----------|------|-------------|----------|
|
|
||||||
| `end` |`[number, number]`| End point of the arc. A point in 3D space | No |
|
|
||||||
| `interior` |`[number, number]`| Interior point of the arc. A point in 3D space | No |
|
|
||||||
|
|
||||||
|
|
@ -180,7 +180,7 @@ A plane.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
|
| `type` |enum: `Plane`| | No |
|
||||||
| `id` |`string`| The id of the plane. | No |
|
| `id` |`string`| The id of the plane. | No |
|
||||||
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
|
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
|
||||||
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Plane"
|
|
||||||
excerpt: "A plane."
|
|
||||||
layout: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
A plane.
|
|
||||||
|
|
||||||
**Type:** `object`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Properties
|
|
||||||
|
|
||||||
| Property | Type | Description | Required |
|
|
||||||
|----------|------|-------------|----------|
|
|
||||||
| `id` |`string`| The id of the plane. | No |
|
|
||||||
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A plane. | No |
|
|
||||||
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
|
||||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
|
||||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
|
||||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
|
||||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
|
||||||
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "PlaneData"
|
title: "PlaneData"
|
||||||
excerpt: "Orientation data that can be used to construct a plane, not a plane in itself."
|
excerpt: "Data for a plane."
|
||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
Orientation data that can be used to construct a plane, not a plane in itself.
|
Data for a plane.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,18 +22,6 @@ Data for start sketch on. You can start a sketch on a plane or an solid.
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
----
|
|
||||||
Data for start sketch on. You can start a sketch on a plane or an solid.
|
|
||||||
|
|
||||||
[`Plane`](/docs/kcl/types/Plane)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
Data for start sketch on. You can start a sketch on a plane or an solid.
|
Data for start sketch on. You can start a sketch on a plane or an solid.
|
||||||
|
|
||||||
|
@ -62,8 +62,6 @@ test(
|
|||||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
||||||
// The open file's name is `main.kcl`, so the export file name should be `main.gltf`
|
|
||||||
const exportFileName = `main.gltf`
|
|
||||||
|
|
||||||
// Click the export button
|
// Click the export button
|
||||||
await exportButton.click()
|
await exportButton.click()
|
||||||
@ -98,7 +96,7 @@ test(
|
|||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const outputGltf = await fsp.readFile(exportFileName)
|
const outputGltf = await fsp.readFile('output.gltf')
|
||||||
return outputGltf.byteLength
|
return outputGltf.byteLength
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 0
|
return 0
|
||||||
@ -108,8 +106,8 @@ test(
|
|||||||
)
|
)
|
||||||
.toBeGreaterThan(300_000)
|
.toBeGreaterThan(300_000)
|
||||||
|
|
||||||
// clean up exported file
|
// clean up output.gltf
|
||||||
await fsp.rm(exportFileName)
|
await fsp.rm('output.gltf')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -140,8 +138,6 @@ test(
|
|||||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
||||||
// The open file's name is `other.kcl`, so the export file name should be `other.gltf`
|
|
||||||
const exportFileName = `other.gltf`
|
|
||||||
|
|
||||||
// Click the export button
|
// Click the export button
|
||||||
await exportButton.click()
|
await exportButton.click()
|
||||||
@ -175,7 +171,7 @@ test(
|
|||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const outputGltf = await fsp.readFile(exportFileName)
|
const outputGltf = await fsp.readFile('output.gltf')
|
||||||
return outputGltf.byteLength
|
return outputGltf.byteLength
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 0
|
return 0
|
||||||
@ -185,8 +181,8 @@ test(
|
|||||||
)
|
)
|
||||||
.toBeGreaterThan(100_000)
|
.toBeGreaterThan(100_000)
|
||||||
|
|
||||||
// clean up exported file
|
// clean up output.gltf
|
||||||
await fsp.rm(exportFileName)
|
await fsp.rm('output.gltf')
|
||||||
})
|
})
|
||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
})
|
})
|
||||||
|
@ -1135,189 +1135,3 @@ _test.describe('Deleting items from the file pane', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
_test.describe(
|
|
||||||
'Undo and redo do not keep history when navigating between files',
|
|
||||||
() => {
|
|
||||||
_test(
|
|
||||||
`open a file, change something, open a different file, hitting undo should do nothing`,
|
|
||||||
{ tag: '@electron' },
|
|
||||||
async ({ browserName }, testInfo) => {
|
|
||||||
const { page } = await setupElectron({
|
|
||||||
testInfo,
|
|
||||||
folderSetupFn: async (dir) => {
|
|
||||||
const testDir = join(dir, 'testProject')
|
|
||||||
await fsp.mkdir(testDir, { recursive: true })
|
|
||||||
await fsp.copyFile(
|
|
||||||
executorInputPath('cylinder.kcl'),
|
|
||||||
join(testDir, 'main.kcl')
|
|
||||||
)
|
|
||||||
await fsp.copyFile(
|
|
||||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
|
||||||
join(testDir, 'other.kcl')
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
|
||||||
page.on('console', console.log)
|
|
||||||
|
|
||||||
// Constants and locators
|
|
||||||
const projectCard = page.getByText('testProject')
|
|
||||||
const otherFile = page
|
|
||||||
.getByRole('listitem')
|
|
||||||
.filter({ has: page.getByRole('button', { name: 'other.kcl' }) })
|
|
||||||
|
|
||||||
await _test.step(
|
|
||||||
'Open project and make a change to the file',
|
|
||||||
async () => {
|
|
||||||
await projectCard.click()
|
|
||||||
await u.waitForPageLoad()
|
|
||||||
|
|
||||||
// Get the text in the code locator.
|
|
||||||
const originalText = await u.codeLocator.innerText()
|
|
||||||
// Click in the editor and add some new lines.
|
|
||||||
await u.codeLocator.click()
|
|
||||||
|
|
||||||
await page.keyboard.type(`sketch001 = startSketchOn('XY')
|
|
||||||
some other shit`)
|
|
||||||
|
|
||||||
// Ensure the content in the editor changed.
|
|
||||||
const newContent = await u.codeLocator.innerText()
|
|
||||||
|
|
||||||
expect(originalText !== newContent)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await _test.step('navigate to other.kcl', async () => {
|
|
||||||
await u.openFilePanel()
|
|
||||||
|
|
||||||
await otherFile.click()
|
|
||||||
await u.waitForPageLoad()
|
|
||||||
await u.openKclCodePanel()
|
|
||||||
await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
|
|
||||||
})
|
|
||||||
|
|
||||||
await _test.step('hit undo', async () => {
|
|
||||||
// Get the original content of the file.
|
|
||||||
const originalText = await u.codeLocator.innerText()
|
|
||||||
// Now hit undo
|
|
||||||
await page.keyboard.down('ControlOrMeta')
|
|
||||||
await page.keyboard.press('KeyZ')
|
|
||||||
await page.keyboard.up('ControlOrMeta')
|
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await expect(u.codeLocator).toContainText(originalText)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
_test(
|
|
||||||
`open a file, change something, undo it, open a different file, hitting redo should do nothing`,
|
|
||||||
{ tag: '@electron' },
|
|
||||||
// Skip on windows i think the keybindings are different for redo.
|
|
||||||
async ({ browserName }, testInfo) => {
|
|
||||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
|
||||||
const { page } = await setupElectron({
|
|
||||||
testInfo,
|
|
||||||
folderSetupFn: async (dir) => {
|
|
||||||
const testDir = join(dir, 'testProject')
|
|
||||||
await fsp.mkdir(testDir, { recursive: true })
|
|
||||||
await fsp.copyFile(
|
|
||||||
executorInputPath('cylinder.kcl'),
|
|
||||||
join(testDir, 'main.kcl')
|
|
||||||
)
|
|
||||||
await fsp.copyFile(
|
|
||||||
executorInputPath('basic_fillet_cube_end.kcl'),
|
|
||||||
join(testDir, 'other.kcl')
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
|
||||||
page.on('console', console.log)
|
|
||||||
|
|
||||||
// Constants and locators
|
|
||||||
const projectCard = page.getByText('testProject')
|
|
||||||
const otherFile = page
|
|
||||||
.getByRole('listitem')
|
|
||||||
.filter({ has: page.getByRole('button', { name: 'other.kcl' }) })
|
|
||||||
|
|
||||||
const badContent = 'this shit'
|
|
||||||
await _test.step(
|
|
||||||
'Open project and make a change to the file',
|
|
||||||
async () => {
|
|
||||||
await projectCard.click()
|
|
||||||
await u.waitForPageLoad()
|
|
||||||
|
|
||||||
// Get the text in the code locator.
|
|
||||||
const originalText = await u.codeLocator.innerText()
|
|
||||||
// Click in the editor and add some new lines.
|
|
||||||
await u.codeLocator.click()
|
|
||||||
|
|
||||||
await page.keyboard.type(badContent)
|
|
||||||
|
|
||||||
// Ensure the content in the editor changed.
|
|
||||||
const newContent = await u.codeLocator.innerText()
|
|
||||||
|
|
||||||
expect(originalText !== newContent)
|
|
||||||
|
|
||||||
// Now hit undo
|
|
||||||
await page.keyboard.down('ControlOrMeta')
|
|
||||||
await page.keyboard.press('KeyZ')
|
|
||||||
await page.keyboard.up('ControlOrMeta')
|
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await expect(u.codeLocator).toContainText(originalText)
|
|
||||||
await expect(u.codeLocator).not.toContainText(badContent)
|
|
||||||
|
|
||||||
// Hit redo.
|
|
||||||
await page.keyboard.down('Shift')
|
|
||||||
await page.keyboard.down('ControlOrMeta')
|
|
||||||
await page.keyboard.press('KeyZ')
|
|
||||||
await page.keyboard.up('ControlOrMeta')
|
|
||||||
await page.keyboard.up('Shift')
|
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await expect(u.codeLocator).toContainText(originalText)
|
|
||||||
await expect(u.codeLocator).toContainText(badContent)
|
|
||||||
|
|
||||||
// Now hit undo
|
|
||||||
await page.keyboard.down('ControlOrMeta')
|
|
||||||
await page.keyboard.press('KeyZ')
|
|
||||||
await page.keyboard.up('ControlOrMeta')
|
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await expect(u.codeLocator).toContainText(originalText)
|
|
||||||
await expect(u.codeLocator).not.toContainText(badContent)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await _test.step('navigate to other.kcl', async () => {
|
|
||||||
await u.openFilePanel()
|
|
||||||
|
|
||||||
await otherFile.click()
|
|
||||||
await u.waitForPageLoad()
|
|
||||||
await u.openKclCodePanel()
|
|
||||||
await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
|
|
||||||
await expect(u.codeLocator).not.toContainText(badContent)
|
|
||||||
})
|
|
||||||
|
|
||||||
await _test.step('hit redo', async () => {
|
|
||||||
// Get the original content of the file.
|
|
||||||
const originalText = await u.codeLocator.innerText()
|
|
||||||
// Now hit redo
|
|
||||||
await page.keyboard.down('Shift')
|
|
||||||
await page.keyboard.down('ControlOrMeta')
|
|
||||||
await page.keyboard.press('KeyZ')
|
|
||||||
await page.keyboard.up('ControlOrMeta')
|
|
||||||
await page.keyboard.up('Shift')
|
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await expect(u.codeLocator).toContainText(originalText)
|
|
||||||
await expect(u.codeLocator).not.toContainText(badContent)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
@ -247,7 +247,7 @@ test.describe('Can export from electron app', () => {
|
|||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const outputGltf = await fsp.readFile('main.gltf')
|
const outputGltf = await fsp.readFile('output.gltf')
|
||||||
return outputGltf.byteLength
|
return outputGltf.byteLength
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 0
|
return 0
|
||||||
@ -257,8 +257,8 @@ test.describe('Can export from electron app', () => {
|
|||||||
)
|
)
|
||||||
.toBeGreaterThan(300_000)
|
.toBeGreaterThan(300_000)
|
||||||
|
|
||||||
// clean up exported file
|
// clean up output.gltf
|
||||||
await fsp.rm('main.gltf')
|
await fsp.rm('output.gltf')
|
||||||
})
|
})
|
||||||
|
|
||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
|
@ -202,35 +202,19 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
const viewport = { width: 1200, height: 500 }
|
||||||
|
await page.setViewportSize(viewport)
|
||||||
|
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
|
|
||||||
await page.waitForTimeout(100)
|
const center = {
|
||||||
await u.openAndClearDebugPanel()
|
x: viewport.width / 2,
|
||||||
await u.sendCustomCmd({
|
y: viewport.height / 2,
|
||||||
type: 'modeling_cmd_req',
|
}
|
||||||
cmd_id: uuidv4(),
|
const modelAreaSize = await u.getModelViewAreaSize()
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_look_at',
|
|
||||||
vantage: { x: 0, y: -1250, z: 580 },
|
|
||||||
center: { x: 0, y: 0, z: 0 },
|
|
||||||
up: { x: 0, y: 0, z: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.sendCustomCmd({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'default_camera_get_settings',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
await u.closeDebugPanel()
|
|
||||||
|
|
||||||
// If we have the code pane open, we should see the code.
|
// If we have the code pane open, we should see the code.
|
||||||
if (openPanes.includes('code')) {
|
if (openPanes.includes('code')) {
|
||||||
@ -244,7 +228,7 @@ test.describe('Sketch tests', () => {
|
|||||||
await expect(u.codeLocator).not.toBeVisible()
|
await expect(u.codeLocator).not.toBeVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPX = [665, 458]
|
const startPX = [center.x + 65, 458]
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
let prevContent = ''
|
let prevContent = ''
|
||||||
@ -255,7 +239,7 @@ test.describe('Sketch tests', () => {
|
|||||||
// Wait for the render.
|
// Wait for the render.
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
// Select the sketch
|
// Select the sketch
|
||||||
await page.mouse.click(700, 370)
|
await page.mouse.click(center.x + 100, 370)
|
||||||
}
|
}
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Edit Sketch' })
|
page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
@ -266,45 +250,74 @@ test.describe('Sketch tests', () => {
|
|||||||
prevContent = await page.locator('.cm-content').innerText()
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_look_at',
|
||||||
|
vantage: { x: 0, y: -1250, z: 580 - modelAreaSize.w },
|
||||||
|
center: { x: 0, y: 0, z: 0 },
|
||||||
|
up: { x: 0, y: 0, z: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await u.sendCustomCmd({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'default_camera_get_settings',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
const step5 = { steps: 5 }
|
const step5 = { steps: 5 }
|
||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||||
|
|
||||||
// drag startProfieAt handle
|
test.step('drag startProfileAt handle', async () => {
|
||||||
await page.mouse.move(startPX[0], startPX[1])
|
await page.mouse.move(startPX[0], startPX[1])
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
await page.mouse.up()
|
await page.mouse.up()
|
||||||
|
if (openPanes.includes('code')) {
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (openPanes.includes('code')) {
|
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// drag line handle
|
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
test.step('drag line handle', async () => {
|
||||||
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
|
||||||
await page.mouse.down()
|
await page.mouse.move(lineEnd.x - 5, lineEnd.y)
|
||||||
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
|
await page.mouse.down()
|
||||||
await page.mouse.up()
|
await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5)
|
||||||
await page.waitForTimeout(100)
|
await page.mouse.up()
|
||||||
if (openPanes.includes('code')) {
|
await page.waitForTimeout(100)
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
if (openPanes.includes('code')) {
|
||||||
prevContent = await page.locator('.cm-content').innerText()
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
}
|
prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// drag tangentialArcTo handle
|
test.step('drag tangentialArcTo handle', async () => {
|
||||||
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
|
||||||
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
|
await page.mouse.move(tangentEnd.x, tangentEnd.y - 5)
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5)
|
await page.mouse.move(
|
||||||
await page.mouse.up()
|
tangentEnd.x + dragPX,
|
||||||
await page.waitForTimeout(100)
|
tangentEnd.y - dragPX,
|
||||||
if (openPanes.includes('code')) {
|
step5
|
||||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
)
|
||||||
}
|
await page.mouse.up()
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
if (openPanes.includes('code')) {
|
||||||
|
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Open the code pane
|
// Open the code pane
|
||||||
await u.openKclCodePanel()
|
await u.openKclCodePanel()
|
||||||
@ -580,7 +593,7 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
const startPX = [665, 458]
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
const dragPX = 30
|
const dragPX = 30
|
||||||
|
|
||||||
@ -596,7 +609,7 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||||
|
|
||||||
// drag startProfieAt handle
|
// drag startProfileAt handle
|
||||||
await page.mouse.move(startPX[0], startPX[1])
|
await page.mouse.move(startPX[0], startPX[1])
|
||||||
await page.mouse.down()
|
await page.mouse.down()
|
||||||
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5)
|
||||||
@ -638,6 +651,7 @@ test.describe('Sketch tests', () => {
|
|||||||
})
|
})
|
||||||
test('Can add multiple sketches', async ({ page }) => {
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
|
|
||||||
const viewportSize = { width: 1200, height: 500 }
|
const viewportSize = { width: 1200, height: 500 }
|
||||||
await page.setViewportSize(viewportSize)
|
await page.setViewportSize(viewportSize)
|
||||||
|
|
||||||
@ -661,15 +675,19 @@ test.describe('Sketch tests', () => {
|
|||||||
200
|
200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
let codeStr = "sketch001 = startSketchOn('XY')"
|
let codeStr = "sketch001 = startSketchOn('XY')"
|
||||||
|
|
||||||
await page.mouse.click(center.x, viewportSize.height * 0.55)
|
await page.mouse.click(center.x - 50, viewportSize.height * 0.55)
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
await click00r(0, 0)
|
const { click00r } = await getMovementUtils({ center, page })
|
||||||
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
|
||||||
|
let coord = await click00r(0, 0)
|
||||||
|
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
await click00r(50, 0)
|
await click00r(50, 0)
|
||||||
@ -698,14 +716,15 @@ test.describe('Sketch tests', () => {
|
|||||||
|
|
||||||
// when exiting the sketch above the camera is still looking down at XY,
|
// when exiting the sketch above the camera is still looking down at XY,
|
||||||
// so selecting the plane again is a bit easier.
|
// so selecting the plane again is a bit easier.
|
||||||
await page.mouse.click(center.x + 200, center.y + 100)
|
await page.mouse.move(center.x - 100, center.y + 50, { steps: 5 })
|
||||||
|
await page.mouse.click(center.x - 100, center.y + 50)
|
||||||
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(600) // TODO detect animation ending, or disable animation
|
||||||
codeStr += "sketch002 = startSketchOn('XY')"
|
codeStr += "sketch002 = startSketchOn('XY')"
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
await click00r(30, 0)
|
coord = await click00r(30, 0)
|
||||||
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
codeStr += ` |> startProfileAt(${coord.kcl}, %)`
|
||||||
await expect(u.codeLocator).toHaveText(codeStr)
|
await expect(u.codeLocator).toHaveText(codeStr)
|
||||||
|
|
||||||
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
||||||
@ -763,20 +782,21 @@ test.describe('Sketch tests', () => {
|
|||||||
await u.updateCamPosition(camPos)
|
await u.updateCamPosition(camPos)
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
|
||||||
|
const center = await u.getCenterOfModelViewArea()
|
||||||
await page.mouse.move(0, 0)
|
await page.mouse.move(0, 0)
|
||||||
|
|
||||||
// select a plane
|
// select a plane
|
||||||
await page.mouse.move(700, 200, { steps: 10 })
|
await page.mouse.move(center.x + 100, 200, { steps: 10 })
|
||||||
await page.mouse.click(700, 200, { delay: 200 })
|
await page.mouse.click(center.x + 100, 200, { delay: 200 })
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`sketch001 = startSketchOn('-XZ')`
|
`sketch001 = startSketchOn('-XZ')`
|
||||||
)
|
)
|
||||||
|
|
||||||
let prevContent = await page.locator('.cm-content').innerText()
|
let prevContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
const pointA = [700, 200]
|
const pointA = [center.x + 100, 200]
|
||||||
const pointB = [900, 200]
|
const pointB = [center.x + 300, 200]
|
||||||
const pointC = [900, 400]
|
const pointC = [center.x + 300, 400]
|
||||||
|
|
||||||
// draw three lines
|
// draw three lines
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
@ -913,7 +933,9 @@ extrude001 = extrude(5, sketch001)
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
|
|
||||||
await page.mouse.click(622, 355)
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
|
await page.mouse.click(center.x + 22, 355)
|
||||||
|
|
||||||
await page.waitForTimeout(800)
|
await page.waitForTimeout(800)
|
||||||
await page.getByText(`END')`).click()
|
await page.getByText(`END')`).click()
|
||||||
@ -1274,44 +1296,3 @@ test2.describe('Sketch mode should be toleratant to syntax errors', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test2.describe(`Sketching with offset planes`, () => {
|
|
||||||
test2(
|
|
||||||
`Can select an offset plane to sketch on`,
|
|
||||||
async ({ app, scene, toolbar, editor }) => {
|
|
||||||
// We seed the scene with a single offset plane
|
|
||||||
await app.initialise(`offsetPlane001 = offsetPlane("XY", 10)`)
|
|
||||||
|
|
||||||
const [planeClick, planeHover] = scene.makeMouseHelpers(650, 200)
|
|
||||||
|
|
||||||
await test2.step(`Start sketching on the offset plane`, async () => {
|
|
||||||
await toolbar.startSketchPlaneSelection()
|
|
||||||
|
|
||||||
await test2.step(`Hovering should highlight code`, async () => {
|
|
||||||
await planeHover()
|
|
||||||
await editor.expectState({
|
|
||||||
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
|
|
||||||
diagnostics: [],
|
|
||||||
highlightedCode: 'offsetPlane("XY", 10)',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await test2.step(
|
|
||||||
`Clicking should select the plane and enter sketch mode`,
|
|
||||||
async () => {
|
|
||||||
await planeClick()
|
|
||||||
// Have to wait for engine-side animation to finish
|
|
||||||
await app.page.waitForTimeout(600)
|
|
||||||
await expect2(toolbar.lineBtn).toBeEnabled()
|
|
||||||
await editor.expectEditor.toContain('startSketchOn(offsetPlane001)')
|
|
||||||
await editor.expectState({
|
|
||||||
activeLines: [`offsetPlane001=offsetPlane("XY",10)`],
|
|
||||||
diagnostics: [],
|
|
||||||
highlightedCode: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
@ -283,7 +283,7 @@ part001 = startSketchOn('-XZ')
|
|||||||
const gltfFilename = filenames.filter((t: string) =>
|
const gltfFilename = filenames.filter((t: string) =>
|
||||||
t.includes('.gltf')
|
t.includes('.gltf')
|
||||||
)[0]
|
)[0]
|
||||||
if (!gltfFilename) throw new Error('No gLTF in this archive')
|
if (!gltfFilename) throw new Error('No output.gltf in this archive')
|
||||||
cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${parentPath}/${gltfFilename} ${imagePath}`
|
cliCommand = `export ZOO_TOKEN=${secrets.snapshottoken} && zoo file snapshot --output-format=png --src-format=${outputType} ${parentPath}/${gltfFilename} ${imagePath}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@ -8,6 +8,21 @@ import {
|
|||||||
Locator,
|
Locator,
|
||||||
test,
|
test,
|
||||||
} from '@playwright/test'
|
} from '@playwright/test'
|
||||||
|
import {
|
||||||
|
OrthographicCamera,
|
||||||
|
Mesh,
|
||||||
|
Scene,
|
||||||
|
Raycaster,
|
||||||
|
PlaneGeometry,
|
||||||
|
MeshBasicMaterial,
|
||||||
|
DoubleSide,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
} from 'three'
|
||||||
|
import {
|
||||||
|
RAYCASTABLE_PLANE,
|
||||||
|
INTERSECTION_PLANE_LAYER,
|
||||||
|
} from 'clientSideScene/constants'
|
||||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import fsSync from 'fs'
|
import fsSync from 'fs'
|
||||||
@ -257,52 +272,138 @@ export const circleMove = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMovementUtils = (opts: any) => {
|
export function rollingRound(n: number, digitsAfterDecimal: number) {
|
||||||
// The way we truncate is kinda odd apparently, so we need this function
|
const s = String(n).split('.')
|
||||||
// "[k]itty[c]ad round"
|
|
||||||
const kcRound = (n: number) => Math.trunc(n * 100) / 100
|
|
||||||
|
|
||||||
// To translate between screen and engine ("[U]nit") coordinates
|
// There are no decimals, just return the number.
|
||||||
// NOTE: these pretty much can't be perfect because of screen scaling.
|
if (s.length === 1) return n
|
||||||
// Handle on a case-by-case.
|
|
||||||
const toU = (x: number, y: number) => [
|
|
||||||
kcRound(x * 0.0678),
|
|
||||||
kcRound(-y * 0.0678), // Y is inverted in our coordinate system
|
|
||||||
]
|
|
||||||
|
|
||||||
// Turn the array into a string with specific formatting
|
// Find the closest 9. We don't care about anything beyond that.
|
||||||
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
|
const nineIndex = s[1].indexOf('9')
|
||||||
|
|
||||||
// Combine because used often
|
const fractStr = nineIndex > 0 ? s[1].slice(0, nineIndex + 1) : s[1]
|
||||||
const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
|
|
||||||
|
let fract = Number(fractStr) / 10 ** fractStr.length
|
||||||
|
|
||||||
|
for (let i = fractStr.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (i === digitsAfterDecimal) break
|
||||||
|
fract = Math.round(fract * 10 ** i) / 10 ** i
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Number(s[0]) + fract).toFixed(digitsAfterDecimal)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMovementUtils = async (opts: any) => {
|
||||||
|
const sceneInfra = await opts.page.evaluate(() => window.sceneInfra)
|
||||||
|
|
||||||
|
// Various data for raycasting into the scene to get our XY.
|
||||||
|
const hundredM = 100_0000
|
||||||
|
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
|
||||||
|
const planeMaterial = new MeshBasicMaterial({
|
||||||
|
color: 0xff0000,
|
||||||
|
side: DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
})
|
||||||
|
const scene = new Scene()
|
||||||
|
const intersectionPlane = new Mesh(planeGeometry, planeMaterial)
|
||||||
|
intersectionPlane.userData = { type: RAYCASTABLE_PLANE }
|
||||||
|
intersectionPlane.name = RAYCASTABLE_PLANE
|
||||||
|
intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
|
||||||
|
scene.add(intersectionPlane)
|
||||||
|
const planeRaycaster = new Raycaster()
|
||||||
|
planeRaycaster.far = Infinity
|
||||||
|
planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
|
const kcRound = (n: number) => Math.round(n * 100) / 100
|
||||||
|
|
||||||
// Make it easier to click around from center ("click [from] zero zero")
|
// Make it easier to click around from center ("click [from] zero zero")
|
||||||
const click00 = (x: number, y: number) =>
|
const click00 = (x: number, y: number) =>
|
||||||
opts.page.mouse.click(opts.center.x + x, opts.center.y + y, { delay: 100 })
|
opts.page.mouse.click(x, y, { delay: 100 })
|
||||||
|
|
||||||
// Relative clicker, must keep state
|
// Relative clicker, must keep state
|
||||||
let last = { x: 0, y: 0 }
|
let last = { x: 0, y: 0 }
|
||||||
|
let lastScreenSpace = { x: 0, y: 0 }
|
||||||
|
|
||||||
const click00r = async (x?: number, y?: number) => {
|
const click00r = async (x?: number, y?: number) => {
|
||||||
// reset relative coordinates when anything is undefined
|
// reset relative coordinates when anything is undefined
|
||||||
if (x === undefined || y === undefined) {
|
if (x === undefined || y === undefined) {
|
||||||
last.x = 0
|
last = { x: 0, y: 0 }
|
||||||
last.y = 0
|
lastScreenSpace = { x: 0, y: 0 }
|
||||||
return
|
return {
|
||||||
|
nextXY: [0, 0],
|
||||||
|
kcl: `[0, 0]`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await circleMove(
|
const absX = opts.center.x + x
|
||||||
opts.page,
|
const absY = opts.center.y + y
|
||||||
opts.center.x + last.x + x,
|
|
||||||
opts.center.y + last.y + y,
|
const nextX = last.x + x
|
||||||
10,
|
const nextY = last.y + y
|
||||||
10
|
|
||||||
|
const targetX = opts.center.x + nextX
|
||||||
|
const targetY = opts.center.y + -nextY
|
||||||
|
|
||||||
|
// Use the current camera specification
|
||||||
|
const camera = await opts.page.evaluate(() => {
|
||||||
|
window.sceneInfra.camControls.onCameraChange(true)
|
||||||
|
return window.sceneInfra.camControls.camera
|
||||||
|
})
|
||||||
|
|
||||||
|
const windowWH = await opts.page.evaluate(() => ({
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// I didn't write this math, it's copied from sceneInfra.ts, and I understand
|
||||||
|
// it's just normalizing the point, but why *-2 ± 1 I have no idea.
|
||||||
|
const mouseVector = new Vector2(
|
||||||
|
(targetX / windowWH.w) * 2 - 1,
|
||||||
|
-(targetY / windowWH.h) * 2 + 1
|
||||||
)
|
)
|
||||||
await click00(last.x + x, last.y + y)
|
planeRaycaster.setFromCamera(mouseVector, camera)
|
||||||
|
const intersections = planeRaycaster.intersectObjects(scene.children, true)
|
||||||
|
|
||||||
|
const planePosition = intersections[0].object.position
|
||||||
|
const inversePlaneQuaternion = intersections[0].object.quaternion
|
||||||
|
.clone()
|
||||||
|
.invert()
|
||||||
|
let transformedPoint = intersections[0].point.clone()
|
||||||
|
if (transformedPoint) {
|
||||||
|
transformedPoint.applyQuaternion(inversePlaneQuaternion)
|
||||||
|
}
|
||||||
|
const twoD = new Vector2(
|
||||||
|
// I think the intersection plane doesn't get scale when nearly everything else does, maybe that should change
|
||||||
|
transformedPoint.x / sceneInfra._baseUnitMultiplier,
|
||||||
|
transformedPoint.y / sceneInfra._baseUnitMultiplier
|
||||||
|
) // z should be 0
|
||||||
|
const planePositionCorrected = new Vector3(
|
||||||
|
...planePosition
|
||||||
|
).applyQuaternion(inversePlaneQuaternion)
|
||||||
|
twoD.sub(new Vector2(...planePositionCorrected))
|
||||||
|
|
||||||
|
await circleMove(opts.page, targetX, targetY, 10, 10)
|
||||||
|
await click00(targetX, targetY)
|
||||||
|
|
||||||
last.x += x
|
last.x += x
|
||||||
last.y += y
|
last.y += y
|
||||||
|
|
||||||
// Returns the new absolute coordinate if you need it.
|
const relativeScreenSpace = {
|
||||||
return [last.x, last.y]
|
x: twoD.x - lastScreenSpace.x,
|
||||||
|
y: -(twoD.y - lastScreenSpace.y),
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScreenSpace.x = kcRound(twoD.x)
|
||||||
|
lastScreenSpace.y = kcRound(twoD.y)
|
||||||
|
|
||||||
|
// Returns the new absolute coordinate and the screen space coordinate if you need it.
|
||||||
|
return {
|
||||||
|
nextXY: [last.x, last.y],
|
||||||
|
kcl: `[${kcRound(relativeScreenSpace.x)}, ${-kcRound(
|
||||||
|
relativeScreenSpace.y
|
||||||
|
)}]`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { toSU, toU, click00r }
|
return { toSU, toU, click00r }
|
||||||
@ -356,6 +457,30 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
||||||
|
|
||||||
const util = {
|
const util = {
|
||||||
|
async getModelViewAreaSize() {
|
||||||
|
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
|
||||||
|
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
|
||||||
|
|
||||||
|
const sidebar = page.getByTestId('modeling-sidebar')
|
||||||
|
const bb = await sidebar.boundingBox()
|
||||||
|
return {
|
||||||
|
w: windowInnerWidth - (bb?.width ?? 0),
|
||||||
|
h: windowInnerHeight - (bb?.height ?? 0),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getCenterOfModelViewArea() {
|
||||||
|
const windowInnerWidth = await page.evaluate(() => window.innerWidth)
|
||||||
|
const windowInnerHeight = await page.evaluate(() => window.innerHeight)
|
||||||
|
|
||||||
|
const sidebar = page.getByTestId('modeling-sidebar')
|
||||||
|
const bb = await sidebar.boundingBox()
|
||||||
|
const goRightPx = (bb?.width ?? 0) / 2
|
||||||
|
const borderWidthsCombined = 2
|
||||||
|
return {
|
||||||
|
x: Math.round(windowInnerWidth / 2 + goRightPx) - borderWidthsCombined,
|
||||||
|
y: Math.round(windowInnerHeight / 2),
|
||||||
|
}
|
||||||
|
},
|
||||||
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
||||||
waitForPageLoad: () => waitForPageLoad(page),
|
waitForPageLoad: () => waitForPageLoad(page),
|
||||||
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
||||||
|
@ -43,10 +43,12 @@ test.describe('Testing constraints', () => {
|
|||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(500) // wait for animation
|
await page.waitForTimeout(500) // wait for animation
|
||||||
|
|
||||||
const startXPx = 500
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
|
const startXPx = center.x - 100
|
||||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||||
await page.keyboard.down('Shift')
|
await page.keyboard.down('Shift')
|
||||||
await page.mouse.click(834, 244)
|
await page.mouse.click(center.x + 234, 244)
|
||||||
await page.keyboard.up('Shift')
|
await page.keyboard.up('Shift')
|
||||||
|
|
||||||
await page
|
await page
|
||||||
|
@ -743,18 +743,19 @@ extrude001 = extrude(5, sketch001)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
// Selectors and constants
|
// Selectors and constants
|
||||||
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
||||||
const lineToolButton = page.getByTestId('line')
|
const lineToolButton = page.getByTestId('line')
|
||||||
const segmentOverlays = page.getByTestId('segment-overlay')
|
const segmentOverlays = page.getByTestId('segment-overlay')
|
||||||
const sketchOriginLocation = { x: 600, y: 250 }
|
const sketchOriginLocation = await u.getCenterOfModelViewArea()
|
||||||
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
||||||
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
||||||
|
|
||||||
await test.step(`Get into sketch mode`, async () => {
|
await test.step(`Get into sketch mode`, async () => {
|
||||||
await u.waitForAuthSkipAppStart()
|
await page.mouse.click(sketchOriginLocation.x, sketchOriginLocation.y)
|
||||||
await page.mouse.click(700, 200)
|
|
||||||
await expect(editSketchButton).toBeVisible()
|
await expect(editSketchButton).toBeVisible()
|
||||||
await editSketchButton.click()
|
await editSketchButton.click()
|
||||||
|
|
||||||
@ -765,12 +766,18 @@ extrude001 = extrude(5, sketch001)
|
|||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="${0}"]`, 0)
|
||||||
|
|
||||||
|
// Our lines are translucent (surprise!), so we need to get on portion
|
||||||
|
// of the line that is only on the background, and not on top of something
|
||||||
|
// like the axis lines.
|
||||||
|
line1.x -= 1
|
||||||
|
line1.y -= 1
|
||||||
|
|
||||||
await test.step(`Check the sketch line color before`, async () => {
|
await test.step(`Check the sketch line color before`, async () => {
|
||||||
await expect
|
await expect
|
||||||
.poll(() =>
|
.poll(() => u.getGreatestPixDiff(line1, darkThemeSegmentColor))
|
||||||
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
|
.toBeLessThanOrEqual(34)
|
||||||
)
|
|
||||||
.toBeLessThan(15)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Change theme to light using command palette`, async () => {
|
await test.step(`Change theme to light using command palette`, async () => {
|
||||||
@ -785,10 +792,8 @@ extrude001 = extrude(5, sketch001)
|
|||||||
|
|
||||||
await test.step(`Check the sketch line color after`, async () => {
|
await test.step(`Check the sketch line color after`, async () => {
|
||||||
await expect
|
await expect
|
||||||
.poll(() =>
|
.poll(() => u.getGreatestPixDiff(line1, lightThemeSegmentColor))
|
||||||
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
|
.toBeLessThanOrEqual(34)
|
||||||
)
|
|
||||||
.toBeLessThan(15)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -503,14 +503,16 @@ test('Sketch on face', async ({ page }) => {
|
|||||||
|
|
||||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||||
|
|
||||||
await u.openAndClearDebugPanel()
|
const center = await u.getCenterOfModelViewArea()
|
||||||
|
|
||||||
|
// This basically waits for sketch mode to be ready.
|
||||||
await u.doAndWaitForCmd(
|
await u.doAndWaitForCmd(
|
||||||
() => page.mouse.click(625, 165),
|
async () => page.mouse.click(center.x, 180),
|
||||||
'default_camera_get_settings',
|
'default_camera_get_settings',
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
await page.waitForTimeout(150)
|
|
||||||
await u.closeDebugPanel()
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
const firstClickPosition = [612, 238]
|
const firstClickPosition = [612, 238]
|
||||||
const secondClickPosition = [661, 242]
|
const secondClickPosition = [661, 242]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zoo-modeling-app",
|
"name": "zoo-modeling-app",
|
||||||
"version": "0.26.6",
|
"version": "0.26.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"author": {
|
"author": {
|
||||||
@ -40,7 +40,7 @@
|
|||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"decamelize": "^6.0.0",
|
"decamelize": "^6.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"electron-updater": "6.3.0",
|
"electron-updater": "^6.3.9",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"html2canvas-pro": "^1.5.8",
|
"html2canvas-pro": "^1.5.8",
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
|
46
src/App.tsx
@ -1,15 +1,14 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||||
import { Stream } from './components/Stream'
|
|
||||||
import { AppHeader } from './components/AppHeader'
|
import { AppHeader } from './components/AppHeader'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { type IndexLoaderData } from 'lib/types'
|
import { type IndexLoaderData } from 'lib/types'
|
||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
import { codeManager, engineCommandManager, sceneInfra } from 'lib/singletons'
|
||||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
@ -22,6 +21,8 @@ import Gizmo from 'components/Gizmo'
|
|||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { UnitsMenu } from 'components/UnitsMenu'
|
import { UnitsMenu } from 'components/UnitsMenu'
|
||||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||||
|
import EngineStreamContext from 'hooks/useEngineStreamContext'
|
||||||
|
import { EngineStream } from 'components/EngineStream'
|
||||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||||
maybeWriteToDisk()
|
maybeWriteToDisk()
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
@ -37,6 +38,13 @@ export function App() {
|
|||||||
// the coredump.
|
// the coredump.
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Stream related refs and data
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const modelingSidebarRef = useRef<HTMLUListElement>(null)
|
||||||
|
let [searchParams] = useSearchParams()
|
||||||
|
const pool = searchParams.get('pool')
|
||||||
|
|
||||||
const projectName = project?.name || null
|
const projectName = project?.name || null
|
||||||
const projectPath = project?.path || null
|
const projectPath = project?.path || null
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -57,6 +65,10 @@ export function App() {
|
|||||||
app: { onboardingStatus },
|
app: { onboardingStatus },
|
||||||
} = settings.context
|
} = settings.context
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sceneInfra.camControls.modelingSidebarRef = modelingSidebarRef
|
||||||
|
}, [modelingSidebarRef.current])
|
||||||
|
|
||||||
useHotkeys('backspace', (e) => {
|
useHotkeys('backspace', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
})
|
})
|
||||||
@ -84,14 +96,26 @@ export function App() {
|
|||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
<ModelingSidebar paneOpacity={paneOpacity} ref={modelingSidebarRef} />
|
||||||
<Stream />
|
<EngineStreamContext.Provider
|
||||||
{/* <CamToggle /> */}
|
options={{
|
||||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
input: {
|
||||||
<UnitsMenu />
|
videoRef,
|
||||||
<Gizmo />
|
canvasRef,
|
||||||
<CameraProjectionToggle />
|
mediaStream: null,
|
||||||
</LowerRightControls>
|
authToken: auth?.context?.token ?? null,
|
||||||
|
pool,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EngineStream />
|
||||||
|
{/* <CamToggle /> */}
|
||||||
|
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||||
|
<UnitsMenu />
|
||||||
|
<Gizmo />
|
||||||
|
<CameraProjectionToggle />
|
||||||
|
</LowerRightControls>
|
||||||
|
</EngineStreamContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from 'lib/toolbar'
|
} from 'lib/toolbar'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export function Toolbar({
|
export function Toolbar({
|
||||||
className = '',
|
className = '',
|
||||||
@ -48,7 +49,7 @@ export function Toolbar({
|
|||||||
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
||||||
|
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState, immediateState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useAppState()
|
const { isStreamReady } = useAppState()
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ export function Toolbar({
|
|||||||
(overallState !== NetworkHealthState.Ok &&
|
(overallState !== NetworkHealthState.Ok &&
|
||||||
overallState !== NetworkHealthState.Weak) ||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
isExecuting ||
|
isExecuting ||
|
||||||
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished ||
|
||||||
!isStreamReady
|
!isStreamReady
|
||||||
|
|
||||||
const currentMode =
|
const currentMode =
|
||||||
@ -141,7 +143,6 @@ export function Toolbar({
|
|||||||
>
|
>
|
||||||
{/* A menu item will either be a vertical line break, a button with a dropdown, or a single button */}
|
{/* A menu item will either be a vertical line break, a button with a dropdown, or a single button */}
|
||||||
{currentModeItems.map((maybeIconConfig, i) => {
|
{currentModeItems.map((maybeIconConfig, i) => {
|
||||||
// Vertical Line Break
|
|
||||||
if (maybeIconConfig === 'break') {
|
if (maybeIconConfig === 'break') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -150,7 +151,6 @@ export function Toolbar({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (Array.isArray(maybeIconConfig)) {
|
} else if (Array.isArray(maybeIconConfig)) {
|
||||||
// A button with a dropdown
|
|
||||||
return (
|
return (
|
||||||
<ActionButtonDropdown
|
<ActionButtonDropdown
|
||||||
Element="button"
|
Element="button"
|
||||||
@ -217,7 +217,6 @@ export function Toolbar({
|
|||||||
}
|
}
|
||||||
const itemConfig = maybeIconConfig
|
const itemConfig = maybeIconConfig
|
||||||
|
|
||||||
// A single button
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" key={itemConfig.id}>
|
<div className="relative" key={itemConfig.id}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { MutableRefObject } from 'react'
|
||||||
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
|
import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls'
|
||||||
import {
|
import {
|
||||||
Euler,
|
Euler,
|
||||||
@ -87,6 +89,9 @@ class CameraRateLimiter {
|
|||||||
|
|
||||||
export class CameraControls {
|
export class CameraControls {
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
|
modelingSidebarRef: MutableRefObject<HTMLUListElement | null> = {
|
||||||
|
current: null,
|
||||||
|
}
|
||||||
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
syncDirection: 'clientToEngine' | 'engineToClient' = 'engineToClient'
|
||||||
camera: PerspectiveCamera | OrthographicCamera
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
target: Vector3
|
target: Vector3
|
||||||
@ -95,6 +100,13 @@ export class CameraControls {
|
|||||||
wasDragging: boolean
|
wasDragging: boolean
|
||||||
mouseDownPosition: Vector2
|
mouseDownPosition: Vector2
|
||||||
mouseNewPosition: Vector2
|
mouseNewPosition: Vector2
|
||||||
|
cameraDragStartXY = new Vector2()
|
||||||
|
old:
|
||||||
|
| {
|
||||||
|
camera: PerspectiveCamera | OrthographicCamera
|
||||||
|
target: Vector3
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
rotationSpeed = 0.3
|
rotationSpeed = 0.3
|
||||||
enableRotate = true
|
enableRotate = true
|
||||||
enablePan = true
|
enablePan = true
|
||||||
@ -461,6 +473,7 @@ export class CameraControls {
|
|||||||
if (this.syncDirection === 'engineToClient') {
|
if (this.syncDirection === 'engineToClient') {
|
||||||
const interaction = this.getInteractionType(event)
|
const interaction = this.getInteractionType(event)
|
||||||
if (interaction === 'none') return
|
if (interaction === 'none') return
|
||||||
|
|
||||||
void this.engineCommandManager.sendSceneCommand({
|
void this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -909,18 +922,123 @@ export class CameraControls {
|
|||||||
up: { x: 0, y: 0, z: 1 },
|
up: { x: 0, y: 0, z: 1 },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await this.engineCommandManager.sendSceneCommand({
|
|
||||||
|
await this.centerModelRelativeToPanes({
|
||||||
|
zoomToFit: true,
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.cameraDragStartXY = new Vector2()
|
||||||
|
this.cameraDragStartXY.x = 0
|
||||||
|
this.cameraDragStartXY.y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreCameraPosition(): Promise<void> {
|
||||||
|
if (!this.old) return
|
||||||
|
|
||||||
|
this.camera = this.old.camera.clone()
|
||||||
|
this.target = this.old.target.clone()
|
||||||
|
|
||||||
|
void this.engineCommandManager.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
cmd: {
|
cmd: {
|
||||||
type: 'zoom_to_fit',
|
type: 'default_camera_look_at',
|
||||||
object_ids: [], // leave empty to zoom to all objects
|
...convertThreeCamValuesToEngineCam({
|
||||||
padding: 0.2, // padding around the objects
|
isPerspective: true,
|
||||||
animated: false, // don't animate the zoom for now
|
position: this.camera.position,
|
||||||
|
quaternion: this.camera.quaternion,
|
||||||
|
zoom: this.camera.zoom,
|
||||||
|
target: this.target,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lastFramePaneWidth: number = 0
|
||||||
|
|
||||||
|
async centerModelRelativeToPanes(args?: {
|
||||||
|
zoomObjectId?: string
|
||||||
|
zoomToFit?: boolean
|
||||||
|
resetLastPaneWidth?: boolean
|
||||||
|
}): Promise<void> {
|
||||||
|
const panes = this.modelingSidebarRef?.current
|
||||||
|
if (!panes) return
|
||||||
|
|
||||||
|
const panesWidth = panes.offsetWidth + panes.offsetLeft
|
||||||
|
|
||||||
|
if (args?.resetLastPaneWidth) {
|
||||||
|
this.lastFramePaneWidth = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const goPx =
|
||||||
|
(panesWidth - this.lastFramePaneWidth) / 2 / window.devicePixelRatio
|
||||||
|
this.lastFramePaneWidth = panesWidth
|
||||||
|
|
||||||
|
// Originally I had tried to use the default_camera_look_at endpoint and
|
||||||
|
// some quaternion math to move the camera right, but it ended up being
|
||||||
|
// overly complicated, and I think the threejs scene also doesn't have the
|
||||||
|
// camera coordinates after a zoom-to-fit... So this is much easier, and
|
||||||
|
// maps better to screen coordinates.
|
||||||
|
|
||||||
|
const requests: Models['ModelingCmdReq_type'][] = [
|
||||||
|
{
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_start',
|
||||||
|
interaction: 'pan',
|
||||||
|
window: { x: goPx < 0 ? -goPx : 0, y: 0 },
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_move',
|
||||||
|
interaction: 'pan',
|
||||||
|
window: {
|
||||||
|
x: goPx < 0 ? 0 : goPx,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (args?.zoomToFit) {
|
||||||
|
requests.unshift({
|
||||||
|
cmd: {
|
||||||
|
type: 'zoom_to_fit',
|
||||||
|
object_ids: args?.zoomObjectId ? [args?.zoomObjectId] : [], // leave empty to zoom to all objects
|
||||||
|
padding: 0.2, // padding around the objects
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.engineCommandManager
|
||||||
|
.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_batch_req',
|
||||||
|
batch_id: uuidv4(),
|
||||||
|
responses: true,
|
||||||
|
requests,
|
||||||
|
})
|
||||||
|
// engineCommandManager can't subscribe to batch responses so we'll send
|
||||||
|
// this one off by its lonesome after.
|
||||||
|
.then(() =>
|
||||||
|
this.engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'camera_drag_end',
|
||||||
|
interaction: 'pan',
|
||||||
|
window: {
|
||||||
|
x: goPx < 0 ? 0 : goPx,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async tweenCameraToQuaternion(
|
async tweenCameraToQuaternion(
|
||||||
targetQuaternion: Quaternion,
|
targetQuaternion: Quaternion,
|
||||||
targetPosition = new Vector3(),
|
targetPosition = new Vector3(),
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { useRef, useEffect, useState, useMemo, Fragment } from 'react'
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
Fragment,
|
||||||
|
} from 'react'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
|
||||||
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
import { cameraMouseDragGuards } from 'lib/cameraControls'
|
||||||
@ -242,6 +249,13 @@ const Overlay = ({
|
|||||||
state.matches({ Sketch: 'Rectangle tool' })
|
state.matches({ Sketch: 'Rectangle tool' })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Line labels will cover the constraints overlay if this is not used.
|
||||||
|
// For each line label, ThreeJS increments each CSS2DObject z-index as they
|
||||||
|
// are added. I have looked into overriding renderOrder and depthTest and
|
||||||
|
// while renderOrder is set, ThreeJS still sets z-index on these 2D objects.
|
||||||
|
// It is easier to set this to a large number, such as a billion.
|
||||||
|
const zIndex = 1000000000
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`absolute w-0 h-0`}>
|
<div className={`absolute w-0 h-0`}>
|
||||||
<div
|
<div
|
||||||
@ -252,6 +266,7 @@ const Overlay = ({
|
|||||||
data-overlay-angle={overlay.angle}
|
data-overlay-angle={overlay.angle}
|
||||||
className="pointer-events-auto absolute w-0 h-0"
|
className="pointer-events-auto absolute w-0 h-0"
|
||||||
style={{
|
style={{
|
||||||
|
zIndex,
|
||||||
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
|
transform: `translate3d(${overlay.windowCoords[0]}px, ${overlay.windowCoords[1]}px, 0)`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
@ -260,6 +275,7 @@ const Overlay = ({
|
|||||||
data-overlay-toolbar-index={overlayIndex}
|
data-overlay-toolbar-index={overlayIndex}
|
||||||
className={`px-0 pointer-events-auto absolute flex gap-1`}
|
className={`px-0 pointer-events-auto absolute flex gap-1`}
|
||||||
style={{
|
style={{
|
||||||
|
zIndex,
|
||||||
transform: `translate3d(calc(${
|
transform: `translate3d(calc(${
|
||||||
overlay.windowCoords[0] + xOffset
|
overlay.windowCoords[0] + xOffset
|
||||||
}px + ${xAlignment}), calc(${
|
}px + ${xAlignment}), calc(${
|
||||||
@ -301,6 +317,7 @@ const Overlay = ({
|
|||||||
*/}
|
*/}
|
||||||
{callExpression?.callee?.name !== 'circle' && (
|
{callExpression?.callee?.name !== 'circle' && (
|
||||||
<SegmentMenu
|
<SegmentMenu
|
||||||
|
style={{ zIndex }}
|
||||||
verticalPosition={
|
verticalPosition={
|
||||||
overlay.windowCoords[1] > window.innerHeight / 2
|
overlay.windowCoords[1] > window.innerHeight / 2
|
||||||
? 'top'
|
? 'top'
|
||||||
@ -442,15 +459,17 @@ const SegmentMenu = ({
|
|||||||
verticalPosition,
|
verticalPosition,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
stdLibFnName,
|
stdLibFnName,
|
||||||
|
style,
|
||||||
}: {
|
}: {
|
||||||
verticalPosition: 'top' | 'bottom'
|
verticalPosition: 'top' | 'bottom'
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
stdLibFnName: string
|
stdLibFnName: string
|
||||||
|
style?: CSSProperties
|
||||||
}) => {
|
}) => {
|
||||||
const { send } = useModelingContext()
|
const { send } = useModelingContext()
|
||||||
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover style={style} className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
|
22
src/clientSideScene/constants.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// 63.5 is definitely a bit of a magic number, play with it until it looked right
|
||||||
|
// if it were 64, that would feel like it's something in the engine where a random
|
||||||
|
// power of 2 is used, but it's the 0.5 seems to make things look much more correct
|
||||||
|
export const ZOOM_MAGIC_NUMBER = 63.5
|
||||||
|
|
||||||
|
export const INTERSECTION_PLANE_LAYER = 1
|
||||||
|
export const SKETCH_LAYER = 2
|
||||||
|
|
||||||
|
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||||
|
|
||||||
|
// redundant types so that it can be changed temporarily but CI will catch the wrong type
|
||||||
|
export const DEBUG_SHOW_INTERSECTION_PLANE: false = false
|
||||||
|
export const DEBUG_SHOW_BOTH_SCENES: false = false
|
||||||
|
|
||||||
|
export const X_AXIS = 'xAxis'
|
||||||
|
export const Y_AXIS = 'yAxis'
|
||||||
|
export const AXIS_GROUP = 'axisGroup'
|
||||||
|
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
|
||||||
|
export const ARROWHEAD = 'arrowhead'
|
||||||
|
export const SEGMENT_LENGTH_LABEL = 'segment-length-label'
|
||||||
|
export const SEGMENT_LENGTH_LABEL_TEXT = 'segment-length-label-text'
|
||||||
|
export const SEGMENT_LENGTH_LABEL_OFFSET_PX = 30
|
@ -2,10 +2,7 @@ import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
|||||||
import {
|
import {
|
||||||
GridHelper,
|
GridHelper,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
OrthographicCamera,
|
|
||||||
PerspectiveCamera,
|
PerspectiveCamera,
|
||||||
Group,
|
|
||||||
Mesh,
|
|
||||||
Quaternion,
|
Quaternion,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three'
|
} from 'three'
|
||||||
@ -28,15 +25,9 @@ export function createGridHelper({
|
|||||||
gridHelper.rotation.x = Math.PI / 2
|
gridHelper.rotation.x = Math.PI / 2
|
||||||
return gridHelper
|
return gridHelper
|
||||||
}
|
}
|
||||||
const fudgeFactor = 72.66985970437086
|
|
||||||
|
|
||||||
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
// Re-export scale.ts
|
||||||
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight
|
export * from './scale'
|
||||||
|
|
||||||
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
|
||||||
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
|
||||||
4000 /
|
|
||||||
window.innerHeight
|
|
||||||
|
|
||||||
export function isQuaternionVertical(q: Quaternion) {
|
export function isQuaternionVertical(q: Quaternion) {
|
||||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||||
|
17
src/clientSideScene/scale.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { OrthographicCamera, PerspectiveCamera, Group, Mesh } from 'three'
|
||||||
|
|
||||||
|
export const fudgeFactor = 72.66985970437086
|
||||||
|
|
||||||
|
export const orthoScale = (
|
||||||
|
cam: OrthographicCamera | PerspectiveCamera,
|
||||||
|
innerHeight?: number
|
||||||
|
) => (0.55 * fudgeFactor) / cam.zoom / (innerHeight ?? window.innerHeight)
|
||||||
|
|
||||||
|
export const perspScale = (
|
||||||
|
cam: PerspectiveCamera,
|
||||||
|
group: Group | Mesh,
|
||||||
|
innerHeight?: number
|
||||||
|
) =>
|
||||||
|
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
||||||
|
4000 /
|
||||||
|
(innerHeight ?? window.innerHeight)
|
@ -89,7 +89,6 @@ import { EngineCommandManager } from 'lang/std/engineConnection'
|
|||||||
import {
|
import {
|
||||||
getRectangleCallExpressions,
|
getRectangleCallExpressions,
|
||||||
updateRectangleSketch,
|
updateRectangleSketch,
|
||||||
updateCenterRectangleSketch,
|
|
||||||
} from 'lib/rectangleTool'
|
} from 'lib/rectangleTool'
|
||||||
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
|
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
@ -1044,174 +1043,6 @@ export class SceneEntities {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
setupDraftCenterRectangle = async (
|
|
||||||
sketchPathToNode: PathToNode,
|
|
||||||
forward: [number, number, number],
|
|
||||||
up: [number, number, number],
|
|
||||||
sketchOrigin: [number, number, number],
|
|
||||||
rectangleOrigin: [x: number, y: number]
|
|
||||||
) => {
|
|
||||||
let _ast = structuredClone(kclManager.ast)
|
|
||||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
|
||||||
_ast,
|
|
||||||
sketchPathToNode || [],
|
|
||||||
'VariableDeclaration'
|
|
||||||
)
|
|
||||||
if (trap(_node1)) return Promise.reject(_node1)
|
|
||||||
|
|
||||||
// startSketchOn already exists
|
|
||||||
const variableDeclarationName =
|
|
||||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
|
||||||
const startSketchOn = _node1.node?.declarations
|
|
||||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
|
||||||
|
|
||||||
const tags: [string, string, string] = [
|
|
||||||
findUniqueName(_ast, 'rectangleSegmentA'),
|
|
||||||
findUniqueName(_ast, 'rectangleSegmentB'),
|
|
||||||
findUniqueName(_ast, 'rectangleSegmentC'),
|
|
||||||
]
|
|
||||||
|
|
||||||
startSketchOn[0].init = createPipeExpression([
|
|
||||||
startSketchOnInit,
|
|
||||||
...getRectangleCallExpressions(rectangleOrigin, tags),
|
|
||||||
])
|
|
||||||
|
|
||||||
let _recastAst = parse(recast(_ast))
|
|
||||||
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
|
||||||
_ast = _recastAst
|
|
||||||
|
|
||||||
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
|
|
||||||
sketchPathToNode,
|
|
||||||
forward,
|
|
||||||
up,
|
|
||||||
position: sketchOrigin,
|
|
||||||
maybeModdedAst: _ast,
|
|
||||||
draftExpressionsIndices: { start: 0, end: 3 },
|
|
||||||
})
|
|
||||||
|
|
||||||
sceneInfra.setCallbacks({
|
|
||||||
onMove: async (args) => {
|
|
||||||
// Update the width and height of the draft rectangle
|
|
||||||
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
|
||||||
pathToNodeTwo[1][0] = 0
|
|
||||||
|
|
||||||
const _node = getNodeFromPath<VariableDeclaration>(
|
|
||||||
truncatedAst,
|
|
||||||
pathToNodeTwo || [],
|
|
||||||
'VariableDeclaration'
|
|
||||||
)
|
|
||||||
if (trap(_node)) return Promise.reject(_node)
|
|
||||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
|
||||||
|
|
||||||
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
|
|
||||||
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
|
|
||||||
|
|
||||||
if (sketchInit.type === 'PipeExpression') {
|
|
||||||
updateCenterRectangleSketch(
|
|
||||||
sketchInit,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
tags[0],
|
|
||||||
rectangleOrigin[0],
|
|
||||||
rectangleOrigin[1]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { execState } = await executeAst({
|
|
||||||
ast: truncatedAst,
|
|
||||||
useFakeExecutor: true,
|
|
||||||
engineCommandManager: this.engineCommandManager,
|
|
||||||
programMemoryOverride,
|
|
||||||
idGenerator: kclManager.execState.idGenerator,
|
|
||||||
})
|
|
||||||
const programMemory = execState.memory
|
|
||||||
this.sceneProgramMemory = programMemory
|
|
||||||
const sketch = sketchFromKclValue(
|
|
||||||
programMemory.get(variableDeclarationName),
|
|
||||||
variableDeclarationName
|
|
||||||
)
|
|
||||||
if (err(sketch)) return Promise.reject(sketch)
|
|
||||||
const sgPaths = sketch.paths
|
|
||||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
||||||
|
|
||||||
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
|
|
||||||
sgPaths.forEach((seg, index) =>
|
|
||||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick: async (args) => {
|
|
||||||
// If there is a valid camera interaction that matches, do that instead
|
|
||||||
const interaction = sceneInfra.camControls.getInteractionType(
|
|
||||||
args.mouseEvent
|
|
||||||
)
|
|
||||||
if (interaction !== 'none') return
|
|
||||||
// Commit the rectangle to the full AST/code and return to sketch.idle
|
|
||||||
const cornerPoint = args.intersectionPoint?.twoD
|
|
||||||
if (!cornerPoint || args.mouseEvent.button !== 0) return
|
|
||||||
|
|
||||||
const x = roundOff((cornerPoint.x || 0) - rectangleOrigin[0])
|
|
||||||
const y = roundOff((cornerPoint.y || 0) - rectangleOrigin[1])
|
|
||||||
|
|
||||||
const _node = getNodeFromPath<VariableDeclaration>(
|
|
||||||
_ast,
|
|
||||||
sketchPathToNode || [],
|
|
||||||
'VariableDeclaration'
|
|
||||||
)
|
|
||||||
if (trap(_node)) return
|
|
||||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
|
||||||
|
|
||||||
if (sketchInit.type === 'PipeExpression') {
|
|
||||||
updateCenterRectangleSketch(
|
|
||||||
sketchInit,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
tags[0],
|
|
||||||
rectangleOrigin[0],
|
|
||||||
rectangleOrigin[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
let _recastAst = parse(recast(_ast))
|
|
||||||
if (trap(_recastAst)) return
|
|
||||||
_ast = _recastAst
|
|
||||||
|
|
||||||
// Update the primary AST and unequip the rectangle tool
|
|
||||||
await kclManager.executeAstMock(_ast)
|
|
||||||
sceneInfra.modelingSend({ type: 'Finish center rectangle' })
|
|
||||||
|
|
||||||
// lee: I had this at the bottom of the function, but it's
|
|
||||||
// possible sketchFromKclValue "fails" when sketching on a face,
|
|
||||||
// and this couldn't wouldn't run.
|
|
||||||
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
|
|
||||||
|
|
||||||
const { execState } = await executeAst({
|
|
||||||
ast: _ast,
|
|
||||||
useFakeExecutor: true,
|
|
||||||
engineCommandManager: this.engineCommandManager,
|
|
||||||
programMemoryOverride,
|
|
||||||
idGenerator: kclManager.execState.idGenerator,
|
|
||||||
})
|
|
||||||
const programMemory = execState.memory
|
|
||||||
|
|
||||||
// Prepare to update the THREEjs scene
|
|
||||||
this.sceneProgramMemory = programMemory
|
|
||||||
const sketch = sketchFromKclValue(
|
|
||||||
programMemory.get(variableDeclarationName),
|
|
||||||
variableDeclarationName
|
|
||||||
)
|
|
||||||
if (err(sketch)) return
|
|
||||||
const sgPaths = sketch.paths
|
|
||||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
|
||||||
|
|
||||||
// Update the starting segment of the THREEjs scene
|
|
||||||
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
|
|
||||||
// Update the rest of the segments of the THREEjs scene
|
|
||||||
sgPaths.forEach((seg, index) =>
|
|
||||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setupDraftCircle = async (
|
setupDraftCircle = async (
|
||||||
sketchPathToNode: PathToNode,
|
sketchPathToNode: PathToNode,
|
||||||
forward: [number, number, number],
|
forward: [number, number, number],
|
||||||
|
@ -291,14 +291,14 @@ export class SceneInfra {
|
|||||||
engineCommandManager
|
engineCommandManager
|
||||||
)
|
)
|
||||||
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
this.camControls.subscribeToCamChange(() => this.onCameraChange())
|
||||||
this.camControls.camera.layers.enable(SKETCH_LAYER)
|
this.camControls.camera.layers.enable(constants.SKETCH_LAYER)
|
||||||
if (DEBUG_SHOW_INTERSECTION_PLANE)
|
if (constants.DEBUG_SHOW_INTERSECTION_PLANE)
|
||||||
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
|
this.camControls.camera.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// RAYCASTERS
|
// RAYCASTERS
|
||||||
this.raycaster.layers.enable(SKETCH_LAYER)
|
this.raycaster.layers.enable(constants.SKETCH_LAYER)
|
||||||
this.raycaster.layers.disable(0)
|
this.raycaster.layers.disable(0)
|
||||||
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
|
this.planeRaycaster.layers.enable(constants.INTERSECTION_PLANE_LAYER)
|
||||||
|
|
||||||
// GRID
|
// GRID
|
||||||
const size = 100
|
const size = 100
|
||||||
@ -333,7 +333,7 @@ export class SceneInfra {
|
|||||||
this.camControls.target
|
this.camControls.target
|
||||||
)
|
)
|
||||||
const axisGroup = this.scene
|
const axisGroup = this.scene
|
||||||
.getObjectByName(AXIS_GROUP)
|
.getObjectByName(constants.AXIS_GROUP)
|
||||||
?.getObjectByName('gridHelper')
|
?.getObjectByName('gridHelper')
|
||||||
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
|
||||||
}
|
}
|
||||||
@ -344,7 +344,6 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
animate = () => {
|
animate = () => {
|
||||||
requestAnimationFrame(this.animate)
|
|
||||||
TWEEN.update() // This will update all tweens during the animation loop
|
TWEEN.update() // This will update all tweens during the animation loop
|
||||||
if (!this.isFovAnimationInProgress) {
|
if (!this.isFovAnimationInProgress) {
|
||||||
// console.log('animation frame', this.cameraControls.camera)
|
// console.log('animation frame', this.cameraControls.camera)
|
||||||
@ -352,6 +351,7 @@ export class SceneInfra {
|
|||||||
this.renderer.render(this.scene, this.camControls.camera)
|
this.renderer.render(this.scene, this.camControls.camera)
|
||||||
this.labelRenderer.render(this.scene, this.camControls.camera)
|
this.labelRenderer.render(this.scene, this.camControls.camera)
|
||||||
}
|
}
|
||||||
|
requestAnimationFrame(this.animate)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose = () => {
|
dispose = () => {
|
||||||
@ -655,11 +655,11 @@ export class SceneInfra {
|
|||||||
}
|
}
|
||||||
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
updateOtherSelectionColors = (otherSelections: Axis[]) => {
|
||||||
const axisGroup = this.scene.children.find(
|
const axisGroup = this.scene.children.find(
|
||||||
({ userData }) => userData?.type === AXIS_GROUP
|
({ userData }) => userData?.type === constants.AXIS_GROUP
|
||||||
)
|
)
|
||||||
const axisMap: { [key: string]: Axis } = {
|
const axisMap: { [key: string]: Axis } = {
|
||||||
[X_AXIS]: 'x-axis',
|
[constants.X_AXIS]: 'x-axis',
|
||||||
[Y_AXIS]: 'y-axis',
|
[constants.Y_AXIS]: 'y-axis',
|
||||||
}
|
}
|
||||||
axisGroup?.children.forEach((_mesh) => {
|
axisGroup?.children.forEach((_mesh) => {
|
||||||
const mesh = _mesh as Mesh
|
const mesh = _mesh as Mesh
|
||||||
|
@ -300,7 +300,7 @@ class StraightSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible,
|
isHandlesVisible: true,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
})
|
})
|
||||||
@ -476,7 +476,7 @@ class TangentialArcToSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible,
|
isHandlesVisible: true,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
angle,
|
angle,
|
||||||
@ -542,7 +542,7 @@ class CircleSegment implements SegmentUtils {
|
|||||||
}
|
}
|
||||||
group.name = CIRCLE_SEGMENT
|
group.name = CIRCLE_SEGMENT
|
||||||
|
|
||||||
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
|
group.add(arcMesh, arrowGroup, circleCenterGroup)
|
||||||
const updateOverlaysCallback = this.update({
|
const updateOverlaysCallback = this.update({
|
||||||
prevSegment,
|
prevSegment,
|
||||||
input,
|
input,
|
||||||
@ -677,7 +677,7 @@ class CircleSegment implements SegmentUtils {
|
|||||||
sceneInfra.updateOverlayDetails({
|
sceneInfra.updateOverlayDetails({
|
||||||
arrowGroup,
|
arrowGroup,
|
||||||
group,
|
group,
|
||||||
isHandlesVisible,
|
isHandlesVisible: true,
|
||||||
from: from,
|
from: from,
|
||||||
to: [center[0], center[1]],
|
to: [center[0], center[1]],
|
||||||
angle: Math.PI / 4,
|
angle: Math.PI / 4,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||||
import { Fragment, useEffect } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
import CommandBarArgument from './CommandBarArgument'
|
import CommandBarArgument from './CommandBarArgument'
|
||||||
import CommandComboBox from '../CommandComboBox'
|
import CommandComboBox from '../CommandComboBox'
|
||||||
import CommandBarReview from './CommandBarReview'
|
import CommandBarReview from './CommandBarReview'
|
||||||
@ -14,6 +16,7 @@ export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
|||||||
export const CommandBar = () => {
|
export const CommandBar = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
const { immediateState } = useNetworkContext()
|
||||||
const {
|
const {
|
||||||
context: { selectedCommand, currentArgument, commands },
|
context: { selectedCommand, currentArgument, commands },
|
||||||
} = commandBarState
|
} = commandBarState
|
||||||
@ -25,6 +28,14 @@ export const CommandBar = () => {
|
|||||||
commandBarSend({ type: 'Close' })
|
commandBarSend({ type: 'Close' })
|
||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
||||||
|
) {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}
|
||||||
|
}, [immediateState])
|
||||||
|
|
||||||
// Hook up keyboard shortcuts
|
// Hook up keyboard shortcuts
|
||||||
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
||||||
if (commandBarState.context.commands.length === 0) return
|
if (commandBarState.context.commands.length === 0) return
|
||||||
|
@ -2,13 +2,20 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import usePlatform from 'hooks/usePlatform'
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
|
||||||
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
import { COMMAND_PALETTE_HOTKEY } from './CommandBar/CommandBar'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { EngineConnectionStateType } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export function CommandBarOpenButton() {
|
export function CommandBarOpenButton() {
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const { immediateState } = useNetworkContext()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
|
const isDisabled =
|
||||||
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
disabled={isDisabled}
|
||||||
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
className="group rounded-full flex items-center justify-center gap-2 px-2 py-1 bg-primary/10 dark:bg-chalkboard-90 dark:backdrop-blur-sm border-primary hover:border-primary dark:border-chalkboard-50 dark:hover:border-inherit text-primary dark:text-inherit"
|
||||||
onClick={() => commandBarSend({ type: 'Open' })}
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
data-testid="command-bar-open-button"
|
data-testid="command-bar-open-button"
|
||||||
|
293
src/components/EngineStream.tsx
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import { MouseEventHandler, useEffect, useRef } from 'react'
|
||||||
|
import { useAppState } from 'AppState'
|
||||||
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
|
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||||
|
import { btnName } from 'lib/cameraControls'
|
||||||
|
import { trap } from 'lib/trap'
|
||||||
|
import { sendSelectEventToEngine } from 'lib/selections'
|
||||||
|
import { kclManager, engineCommandManager } from 'lib/singletons'
|
||||||
|
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
|
||||||
|
import { useRouteLoaderData } from 'react-router-dom'
|
||||||
|
import { PATHS } from 'lib/paths'
|
||||||
|
import { IndexLoaderData } from 'lib/types'
|
||||||
|
import useEngineStreamContext, {
|
||||||
|
EngineStreamState,
|
||||||
|
EngineStreamTransition,
|
||||||
|
} from 'hooks/useEngineStreamContext'
|
||||||
|
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
|
||||||
|
|
||||||
|
export const EngineStream = () => {
|
||||||
|
const { setAppState } = useAppState()
|
||||||
|
|
||||||
|
const { overallState } = useNetworkContext()
|
||||||
|
const { settings } = useSettingsAuthContext()
|
||||||
|
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
|
const last = useRef<number>(Date.now())
|
||||||
|
|
||||||
|
const settingsEngine = {
|
||||||
|
theme: settings.context.app.theme.current,
|
||||||
|
enableSSAO: settings.context.app.enableSSAO.current,
|
||||||
|
highlightEdges: settings.context.modeling.highlightEdges.current,
|
||||||
|
showScaleGrid: settings.context.modeling.showScaleGrid.current,
|
||||||
|
cameraProjection: settings.context.modeling.cameraProjection.current,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { state: modelingMachineState, send: modelingMachineActorSend } =
|
||||||
|
useModelingContext()
|
||||||
|
|
||||||
|
const engineStreamActor = useEngineStreamContext.useActorRef()
|
||||||
|
const engineStreamState = engineStreamActor.getSnapshot()
|
||||||
|
|
||||||
|
const streamIdleMode = settings.context.app.streamIdleMode.current
|
||||||
|
|
||||||
|
const configure = () => {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
|
||||||
|
// It's possible a reconnect happens as we drag the window :')
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const play = () => {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.Play,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
engineCommandManager.addEventListener(
|
||||||
|
EngineCommandManagerEvents.SceneReady,
|
||||||
|
play
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
engineCommandManager.removeEventListener(
|
||||||
|
EngineCommandManagerEvents.SceneReady,
|
||||||
|
play
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const video = engineStreamState.context.videoRef?.current
|
||||||
|
if (!video) return
|
||||||
|
const canvas = engineStreamState.context.canvasRef?.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
new ResizeObserver(() => {
|
||||||
|
if (Date.now() - last.current < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE)
|
||||||
|
return
|
||||||
|
last.current = Date.now()
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(video.width - window.innerWidth) > 4 ||
|
||||||
|
Math.abs(video.height - window.innerHeight) > 4
|
||||||
|
) {
|
||||||
|
timeoutStart.current = Date.now()
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
}).observe(document.body)
|
||||||
|
}, [engineStreamState.value])
|
||||||
|
|
||||||
|
// When the video and canvas element references are set, start the engine.
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
engineStreamState.context.canvasRef.current &&
|
||||||
|
engineStreamState.context.videoRef.current
|
||||||
|
) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
engineStreamState.context.canvasRef.current,
|
||||||
|
engineStreamState.context.videoRef.current,
|
||||||
|
])
|
||||||
|
|
||||||
|
// On settings change, reconfigure the engine. When paused this gets really tricky,
|
||||||
|
// and also requires onMediaStream to be set!
|
||||||
|
useEffect(() => {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [settings.context])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to execute code when the file changes
|
||||||
|
* but only if the scene is already ready.
|
||||||
|
* See onSceneReady for the initial scene setup.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
|
||||||
|
console.log('execute on file change')
|
||||||
|
void kclManager.executeCode(true).catch(trap)
|
||||||
|
}
|
||||||
|
}, [file?.path, engineCommandManager.engineConnection])
|
||||||
|
|
||||||
|
const IDLE_TIME_MS = Number(streamIdleMode)
|
||||||
|
|
||||||
|
// When streamIdleMode is changed, setup or teardown the timeouts
|
||||||
|
const timeoutStart = useRef<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
timeoutStart.current = streamIdleMode ? Date.now() : null
|
||||||
|
}, [streamIdleMode])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let frameId: ReturnType<typeof window.requestAnimationFrame> = 0
|
||||||
|
const frameLoop = () => {
|
||||||
|
// Do not pause if the user is in the middle of an operation
|
||||||
|
if (!modelingMachineState.matches('idle')) {
|
||||||
|
// In fact, stop the timeout, because we don't want to trigger the
|
||||||
|
// pause when we exit the operation.
|
||||||
|
timeoutStart.current = null
|
||||||
|
} else if (timeoutStart.current) {
|
||||||
|
const elapsed = Date.now() - timeoutStart.current
|
||||||
|
if (elapsed >= IDLE_TIME_MS) {
|
||||||
|
timeoutStart.current = null
|
||||||
|
engineStreamActor.send({ type: EngineStreamTransition.Pause })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frameId = window.requestAnimationFrame(frameLoop)
|
||||||
|
}
|
||||||
|
frameId = window.requestAnimationFrame(frameLoop)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(frameId)
|
||||||
|
}
|
||||||
|
}, [modelingMachineState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!streamIdleMode) return
|
||||||
|
|
||||||
|
const onAnyInput = () => {
|
||||||
|
// Just in case it happens in the middle of the user turning off
|
||||||
|
// idle mode.
|
||||||
|
if (!streamIdleMode) {
|
||||||
|
timeoutStart.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (engineStreamState.value === EngineStreamState.Paused) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.StartOrReconfigureEngine,
|
||||||
|
modelingMachineActorSend,
|
||||||
|
settings: settingsEngine,
|
||||||
|
setAppState,
|
||||||
|
onMediaStream(mediaStream: MediaStream) {
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetMediaStream,
|
||||||
|
mediaStream,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutStart.current = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's possible after a reconnect, the user doesn't move their mouse at
|
||||||
|
// all, meaning the timer is not reset to run. We need to set it every
|
||||||
|
// time our effect dependencies change then.
|
||||||
|
timeoutStart.current = Date.now()
|
||||||
|
|
||||||
|
window.document.addEventListener('keydown', onAnyInput)
|
||||||
|
window.document.addEventListener('keyup', onAnyInput)
|
||||||
|
window.document.addEventListener('mousemove', onAnyInput)
|
||||||
|
window.document.addEventListener('mousedown', onAnyInput)
|
||||||
|
window.document.addEventListener('mouseup', onAnyInput)
|
||||||
|
window.document.addEventListener('scroll', onAnyInput)
|
||||||
|
window.document.addEventListener('touchstart', onAnyInput)
|
||||||
|
window.document.addEventListener('touchstop', onAnyInput)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
timeoutStart.current = null
|
||||||
|
window.document.removeEventListener('keydown', onAnyInput)
|
||||||
|
window.document.removeEventListener('keyup', onAnyInput)
|
||||||
|
window.document.removeEventListener('mousemove', onAnyInput)
|
||||||
|
window.document.removeEventListener('mousedown', onAnyInput)
|
||||||
|
window.document.removeEventListener('mouseup', onAnyInput)
|
||||||
|
window.document.removeEventListener('scroll', onAnyInput)
|
||||||
|
window.document.removeEventListener('touchstart', onAnyInput)
|
||||||
|
window.document.removeEventListener('touchstop', onAnyInput)
|
||||||
|
}
|
||||||
|
}, [streamIdleMode, engineStreamState.value])
|
||||||
|
|
||||||
|
const isNetworkOkay =
|
||||||
|
overallState === NetworkHealthState.Ok ||
|
||||||
|
overallState === NetworkHealthState.Weak
|
||||||
|
|
||||||
|
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
if (!isNetworkOkay) return
|
||||||
|
if (!engineStreamState.context.videoRef.current) return
|
||||||
|
if (modelingMachineState.matches('Sketch')) return
|
||||||
|
if (modelingMachineState.matches({ idle: 'showPlanes' })) return
|
||||||
|
|
||||||
|
if (btnName(e.nativeEvent).left) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
sendSelectEventToEngine(e, engineStreamState.context.videoRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
id="stream"
|
||||||
|
data-testid="stream"
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
onContextMenuCapture={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
key={engineStreamActor.id + 'video'}
|
||||||
|
ref={engineStreamState.context.videoRef}
|
||||||
|
controls={false}
|
||||||
|
className="cursor-pointer"
|
||||||
|
disablePictureInPicture
|
||||||
|
id="video-stream"
|
||||||
|
/>
|
||||||
|
<canvas
|
||||||
|
key={engineStreamActor.id + 'canvas'}
|
||||||
|
ref={engineStreamState.context.canvasRef}
|
||||||
|
className="cursor-pointer"
|
||||||
|
id="freeze-frame"
|
||||||
|
>
|
||||||
|
No canvas support
|
||||||
|
</canvas>
|
||||||
|
<ClientSideScene
|
||||||
|
cameraControls={settings.context.modeling.mouseControls.current}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,40 +1,47 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { useEngineCommands } from './EngineCommands'
|
import { useEngineCommands } from './EngineCommands'
|
||||||
import { Spinner } from './Spinner'
|
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
import useEngineStreamContext, {
|
||||||
|
EngineStreamState,
|
||||||
|
} from 'hooks/useEngineStreamContext'
|
||||||
|
import { CommandLogType } from 'lang/std/engineConnection'
|
||||||
|
|
||||||
export const ModelStateIndicator = () => {
|
export const ModelStateIndicator = () => {
|
||||||
const [commands] = useEngineCommands()
|
const [commands] = useEngineCommands()
|
||||||
|
const [isDone, setIsDone] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const engineStreamActor = useEngineStreamContext.useActorRef()
|
||||||
|
const engineStreamState = engineStreamActor.getSnapshot()
|
||||||
|
|
||||||
const lastCommandType = commands[commands.length - 1]?.type
|
const lastCommandType = commands[commands.length - 1]?.type
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastCommandType === CommandLogType.SetDefaultSystemProperties) {
|
||||||
|
setIsDone(false)
|
||||||
|
}
|
||||||
|
if (lastCommandType === CommandLogType.ExecutionDone) {
|
||||||
|
setIsDone(true)
|
||||||
|
}
|
||||||
|
}, [lastCommandType])
|
||||||
|
|
||||||
let className = 'w-6 h-6 '
|
let className = 'w-6 h-6 '
|
||||||
let icon = <Spinner className={className} />
|
let icon = <div className={className}></div>
|
||||||
let dataTestId = 'model-state-indicator'
|
let dataTestId = 'model-state-indicator'
|
||||||
|
|
||||||
if (lastCommandType === 'receive-reliable') {
|
if (engineStreamState.value === EngineStreamState.Paused) {
|
||||||
className +=
|
className += 'text-secondary'
|
||||||
'bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
icon = <CustomIcon data-testid={dataTestId + '-paused'} name="parallel" />
|
||||||
icon = (
|
} else if (engineStreamState.value === EngineStreamState.Resuming) {
|
||||||
<CustomIcon
|
className += 'text-secondary'
|
||||||
data-testid={dataTestId + '-receive-reliable'}
|
icon = <CustomIcon data-testid={dataTestId + '-resuming'} name="parallel" />
|
||||||
name="checkmark"
|
} else if (isDone) {
|
||||||
/>
|
className += 'text-secondary'
|
||||||
)
|
|
||||||
} else if (lastCommandType === 'execution-done') {
|
|
||||||
className +=
|
|
||||||
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
|
||||||
icon = (
|
icon = (
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
data-testid={dataTestId + '-execution-done'}
|
data-testid={dataTestId + '-execution-done'}
|
||||||
name="checkmark"
|
name="checkmark"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (lastCommandType === 'export-done') {
|
|
||||||
className +=
|
|
||||||
'border-6 border border-solid border-chalkboard-60 dark:border-chalkboard-80 bg-chalkboard-20 dark:bg-chalkboard-80 !group-disabled:bg-chalkboard-30 !dark:group-disabled:bg-chalkboard-80 rounded-sm bg-succeed-10/30 dark:bg-succeed'
|
|
||||||
icon = (
|
|
||||||
<CustomIcon data-testid={dataTestId + '-export-done'} name="checkmark" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -20,7 +20,6 @@ import {
|
|||||||
modelingMachine,
|
modelingMachine,
|
||||||
modelingMachineDefaultContext,
|
modelingMachineDefaultContext,
|
||||||
} from 'machines/modelingMachine'
|
} from 'machines/modelingMachine'
|
||||||
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import {
|
import {
|
||||||
isCursorInSketchCommandRange,
|
isCursorInSketchCommandRange,
|
||||||
@ -63,7 +62,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
moveValueIntoNewVariablePath,
|
moveValueIntoNewVariablePath,
|
||||||
sketchOnExtrudedFace,
|
sketchOnExtrudedFace,
|
||||||
sketchOnOffsetPlane,
|
|
||||||
startSketchOnDefault,
|
startSketchOnDefault,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { Program, parse, recast } from 'lang/wasm'
|
import { Program, parse, recast } from 'lang/wasm'
|
||||||
@ -113,13 +111,8 @@ export const ModelingMachineProvider = ({
|
|||||||
auth,
|
auth,
|
||||||
settings: {
|
settings: {
|
||||||
context: {
|
context: {
|
||||||
app: { theme, enableSSAO },
|
app: { theme },
|
||||||
modeling: {
|
modeling: { defaultUnit, highlightEdges, cameraProjection },
|
||||||
defaultUnit,
|
|
||||||
cameraProjection,
|
|
||||||
highlightEdges,
|
|
||||||
showScaleGrid,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
@ -130,9 +123,6 @@ export const ModelingMachineProvider = ({
|
|||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
const persistedContext = useMemo(() => getPersistedContext(), [])
|
const persistedContext = useMemo(() => getPersistedContext(), [])
|
||||||
|
|
||||||
let [searchParams] = useSearchParams()
|
|
||||||
const pool = searchParams.get('pool')
|
|
||||||
|
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
@ -484,7 +474,7 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager.exportInfo = {
|
engineCommandManager.exportInfo = {
|
||||||
intent: ExportIntent.Save,
|
intent: ExportIntent.Save,
|
||||||
// This never gets used its only for make.
|
// This never gets used its only for make.
|
||||||
name: file?.name?.replace('.kcl', `.${event.data.type}`) || '',
|
name: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
const format = {
|
const format = {
|
||||||
@ -637,16 +627,13 @@ export const ModelingMachineProvider = ({
|
|||||||
),
|
),
|
||||||
'animate-to-face': fromPromise(async ({ input }) => {
|
'animate-to-face': fromPromise(async ({ input }) => {
|
||||||
if (!input) return undefined
|
if (!input) return undefined
|
||||||
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
|
if (input.type === 'extrudeFace') {
|
||||||
const sketched =
|
const sketched = sketchOnExtrudedFace(
|
||||||
input.type === 'extrudeFace'
|
kclManager.ast,
|
||||||
? sketchOnExtrudedFace(
|
input.sketchPathToNode,
|
||||||
kclManager.ast,
|
input.extrudePathToNode,
|
||||||
input.sketchPathToNode,
|
input.faceInfo
|
||||||
input.extrudePathToNode,
|
)
|
||||||
input.faceInfo
|
|
||||||
)
|
|
||||||
: sketchOnOffsetPlane(kclManager.ast, input.pathToNode)
|
|
||||||
if (err(sketched)) {
|
if (err(sketched)) {
|
||||||
const sketchedError = new Error(
|
const sketchedError = new Error(
|
||||||
'Incompatible face, please try another'
|
'Incompatible face, please try another'
|
||||||
@ -658,9 +645,13 @@ export const ModelingMachineProvider = ({
|
|||||||
|
|
||||||
await kclManager.executeAstMock(modifiedAst)
|
await kclManager.executeAstMock(modifiedAst)
|
||||||
|
|
||||||
const id =
|
await letEngineAnimateAndSyncCamAfter(
|
||||||
input.type === 'extrudeFace' ? input.faceId : input.planeId
|
engineCommandManager,
|
||||||
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
|
input.faceId
|
||||||
|
)
|
||||||
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNewSketchNode,
|
sketchPathToNode: pathToNewSketchNode,
|
||||||
@ -681,6 +672,9 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
input.planeId
|
input.planeId
|
||||||
)
|
)
|
||||||
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: pathToNode,
|
sketchPathToNode: pathToNode,
|
||||||
@ -703,6 +697,9 @@ export const ModelingMachineProvider = ({
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
info?.sketchDetails?.faceId || ''
|
info?.sketchDetails?.faceId || ''
|
||||||
)
|
)
|
||||||
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
|
resetLastPaneWidth: true,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
sketchPathToNode: sketchPathToNode || [],
|
sketchPathToNode: sketchPathToNode || [],
|
||||||
zAxis: info.sketchDetails.zAxis || null,
|
zAxis: info.sketchDetails.zAxis || null,
|
||||||
@ -1071,21 +1068,6 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
useSetupEngineManager(
|
|
||||||
streamRef,
|
|
||||||
modelingSend,
|
|
||||||
modelingState.context,
|
|
||||||
{
|
|
||||||
pool: pool,
|
|
||||||
theme: theme.current,
|
|
||||||
highlightEdges: highlightEdges.current,
|
|
||||||
enableSSAO: enableSSAO.current,
|
|
||||||
showScaleGrid: showScaleGrid.current,
|
|
||||||
cameraProjection: cameraProjection.current,
|
|
||||||
},
|
|
||||||
token
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kclManager.registerExecuteCallback(() => {
|
kclManager.registerExecuteCallback(() => {
|
||||||
modelingSend({ type: 'Re-execute' })
|
modelingSend({ type: 'Re-execute' })
|
||||||
|
@ -43,7 +43,6 @@ import {
|
|||||||
completionKeymap,
|
completionKeymap,
|
||||||
} from '@codemirror/autocomplete'
|
} from '@codemirror/autocomplete'
|
||||||
import CodeEditor from './CodeEditor'
|
import CodeEditor from './CodeEditor'
|
||||||
import { codeManagerHistoryCompartment } from 'lang/codeManager'
|
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -90,7 +89,7 @@ export const KclEditorPane = () => {
|
|||||||
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
|
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
|
||||||
}),
|
}),
|
||||||
lineHighlightField,
|
lineHighlightField,
|
||||||
codeManagerHistoryCompartment.of(history()),
|
history(),
|
||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
codeFolding(),
|
codeFolding(),
|
||||||
keymap.of([
|
keymap.of([
|
||||||
@ -122,6 +121,7 @@ export const KclEditorPane = () => {
|
|||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
foldGutter(),
|
foldGutter(),
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
indentOnInput(),
|
indentOnInput(),
|
||||||
|
@ -6,6 +6,11 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useContext,
|
useContext,
|
||||||
|
MutableRefObject,
|
||||||
|
forwardRef,
|
||||||
|
// https://stackoverflow.com/a/77055468 Thank you.
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
|
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
|
||||||
@ -19,9 +24,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
|||||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||||
|
import { sceneInfra } from 'lib/singletons'
|
||||||
|
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
|
ref: MutableRefObject<HTMLDivElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BadgeInfoComputed {
|
interface BadgeInfoComputed {
|
||||||
@ -33,19 +41,34 @@ function getPlatformString(): 'web' | 'desktop' {
|
|||||||
return isDesktop() ? 'desktop' : 'web'
|
return isDesktop() ? 'desktop' : 'web'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
export const ModelingSidebar = forwardRef<
|
||||||
|
HTMLUListElement,
|
||||||
|
ModelingSidebarProps
|
||||||
|
>(function ModelingSidebar({ paneOpacity }, outerRef) {
|
||||||
const machineManager = useContext(MachineManagerContext)
|
const machineManager = useContext(MachineManagerContext)
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const kclContext = useKclContext()
|
const kclContext = useKclContext()
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const onboardingStatus = settings.context.app.onboardingStatus
|
const onboardingStatus = settings.context.app.onboardingStatus
|
||||||
const { send, context } = useModelingContext()
|
const { send, state, context } = useModelingContext()
|
||||||
const pointerEventsCssClass =
|
const pointerEventsCssClass =
|
||||||
onboardingStatus.current === 'camera' ||
|
onboardingStatus.current === 'camera' ||
|
||||||
context.store?.openPanes.length === 0
|
context.store?.openPanes.length === 0
|
||||||
? 'pointer-events-none '
|
? 'pointer-events-none '
|
||||||
: 'pointer-events-auto '
|
: 'pointer-events-auto '
|
||||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
const showDebugPanel = settings.context.modeling.showDebugPanel
|
||||||
|
const innerRef = useRef<HTMLUListElement>(null)
|
||||||
|
|
||||||
|
// forwardRef's type causes me to do this type narrowing.
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof outerRef === 'function') {
|
||||||
|
outerRef(innerRef.current)
|
||||||
|
} else {
|
||||||
|
if (outerRef) {
|
||||||
|
outerRef.current = innerRef.current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [innerRef.current])
|
||||||
|
|
||||||
const paneCallbackProps = useMemo(
|
const paneCallbackProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -159,8 +182,37 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
[context.store?.openPanes, send]
|
[context.store?.openPanes, send]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Don't send camera adjustment commands after 1 pane is open. It
|
||||||
|
// won't make any difference.
|
||||||
|
if (context.store?.openPanes.length > 1) return
|
||||||
|
|
||||||
|
void sceneInfra.camControls.centerModelRelativeToPanes()
|
||||||
|
}, [context.store?.openPanes])
|
||||||
|
|
||||||
|
// If the panes are resized then center the model also
|
||||||
|
useEffect(() => {
|
||||||
|
if (!innerRef.current) return
|
||||||
|
|
||||||
|
let last = Date.now()
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
if (Date.now() - last < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE) return
|
||||||
|
if (!innerRef.current) return
|
||||||
|
|
||||||
|
last = Date.now()
|
||||||
|
void sceneInfra.camControls.centerModelRelativeToPanes()
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(innerRef.current)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [state, innerRef.current])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Resizable
|
<Resizable
|
||||||
|
data-testid="modeling-sidebar"
|
||||||
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
||||||
defaultSize={{
|
defaultSize={{
|
||||||
width: '550px',
|
width: '550px',
|
||||||
@ -192,6 +244,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
id="pane-buttons-section"
|
id="pane-buttons-section"
|
||||||
|
data-testid="pane-buttons-section"
|
||||||
className={
|
className={
|
||||||
'w-fit p-2 flex flex-col gap-2 ' +
|
'w-fit p-2 flex flex-col gap-2 ' +
|
||||||
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
|
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
|
||||||
@ -236,6 +289,8 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
</ul>
|
</ul>
|
||||||
<ul
|
<ul
|
||||||
id="pane-section"
|
id="pane-section"
|
||||||
|
data-testid="pane-section"
|
||||||
|
ref={innerRef}
|
||||||
className={
|
className={
|
||||||
'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' +
|
'ml-[-1px] col-start-2 col-span-1 flex flex-col items-stretch gap-2 ' +
|
||||||
(context.store?.openPanes.length >= 1 ? `w-full` : `hidden`)
|
(context.store?.openPanes.length >= 1 ? `w-full` : `hidden`)
|
||||||
@ -265,7 +320,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
interface ModelingPaneButtonProps
|
interface ModelingPaneButtonProps
|
||||||
extends React.HTMLAttributes<HTMLButtonElement> {
|
extends React.HTMLAttributes<HTMLButtonElement> {
|
||||||
|
@ -1,340 +0,0 @@
|
|||||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
|
|
||||||
import Loading from './Loading'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
|
||||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
|
||||||
import { btnName } from 'lib/cameraControls'
|
|
||||||
import { sendSelectEventToEngine } from 'lib/selections'
|
|
||||||
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
|
|
||||||
import { useAppStream } from 'AppState'
|
|
||||||
import {
|
|
||||||
EngineCommandManagerEvents,
|
|
||||||
EngineConnectionStateType,
|
|
||||||
DisconnectingType,
|
|
||||||
} from 'lang/std/engineConnection'
|
|
||||||
import { useRouteLoaderData } from 'react-router-dom'
|
|
||||||
import { PATHS } from 'lib/paths'
|
|
||||||
import { IndexLoaderData } from 'lib/types'
|
|
||||||
|
|
||||||
enum StreamState {
|
|
||||||
Playing = 'playing',
|
|
||||||
Paused = 'paused',
|
|
||||||
Resuming = 'resuming',
|
|
||||||
Unset = 'unset',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Stream = () => {
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
|
||||||
const { settings } = useSettingsAuthContext()
|
|
||||||
const { state, send } = useModelingContext()
|
|
||||||
const { mediaStream } = useAppStream()
|
|
||||||
const { overallState, immediateState } = useNetworkContext()
|
|
||||||
const [streamState, setStreamState] = useState(StreamState.Unset)
|
|
||||||
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
|
||||||
|
|
||||||
const IDLE = settings.context.app.streamIdleMode.current
|
|
||||||
|
|
||||||
const isNetworkOkay =
|
|
||||||
overallState === NetworkHealthState.Ok ||
|
|
||||||
overallState === NetworkHealthState.Weak
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute code and show a "building scene message"
|
|
||||||
* in Stream.tsx in the meantime.
|
|
||||||
*
|
|
||||||
* I would like for this to live somewhere more central,
|
|
||||||
* but it seems to me that we need the video element ref
|
|
||||||
* to be able to play the video after the code has been
|
|
||||||
* executed. If we can find a way to do this from a more
|
|
||||||
* central place, we can move this code there.
|
|
||||||
*/
|
|
||||||
function executeCodeAndPlayStream() {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
kclManager.executeCode(true).then(async () => {
|
|
||||||
await videoRef.current?.play().catch((e) => {
|
|
||||||
console.warn('Video playing was prevented', e, videoRef.current)
|
|
||||||
})
|
|
||||||
setStreamState(StreamState.Playing)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to execute code when the file changes
|
|
||||||
* but only if the scene is already ready.
|
|
||||||
* See onSceneReady for the initial scene setup.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
|
|
||||||
console.log('execute on file change')
|
|
||||||
executeCodeAndPlayStream()
|
|
||||||
}
|
|
||||||
}, [file?.path, engineCommandManager.engineConnection])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
immediateState.type === EngineConnectionStateType.Disconnecting &&
|
|
||||||
immediateState.value.type === DisconnectingType.Pause
|
|
||||||
) {
|
|
||||||
setStreamState(StreamState.Paused)
|
|
||||||
}
|
|
||||||
}, [immediateState])
|
|
||||||
|
|
||||||
// Linux has a default behavior to paste text on middle mouse up
|
|
||||||
// This adds a listener to block that pasting if the click target
|
|
||||||
// is not a text input, so users can move in the 3D scene with
|
|
||||||
// middle mouse drag with a text input focused without pasting.
|
|
||||||
useEffect(() => {
|
|
||||||
const handlePaste = (e: ClipboardEvent) => {
|
|
||||||
const isHtmlElement = e.target && e.target instanceof HTMLElement
|
|
||||||
const isEditable =
|
|
||||||
(isHtmlElement && !('explicitOriginalTarget' in e)) ||
|
|
||||||
('explicitOriginalTarget' in e &&
|
|
||||||
((e.explicitOriginalTarget as HTMLElement).contentEditable ===
|
|
||||||
'true' ||
|
|
||||||
['INPUT', 'TEXTAREA'].some(
|
|
||||||
(tagName) =>
|
|
||||||
tagName === (e.explicitOriginalTarget as HTMLElement).tagName
|
|
||||||
)))
|
|
||||||
if (!isEditable) {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
e.stopImmediatePropagation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis?.window?.document?.addEventListener('paste', handlePaste, {
|
|
||||||
capture: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const IDLE_TIME_MS = 1000 * 60 * 2
|
|
||||||
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
|
|
||||||
|
|
||||||
const teardown = () => {
|
|
||||||
// Already paused
|
|
||||||
if (streamState === StreamState.Paused) return
|
|
||||||
|
|
||||||
videoRef.current?.pause()
|
|
||||||
setStreamState(StreamState.Paused)
|
|
||||||
sceneInfra.modelingSend({ type: 'Cancel' })
|
|
||||||
// Give video time to pause
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
engineCommandManager.tearDown({ idleMode: true })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onVisibilityChange = () => {
|
|
||||||
if (globalThis.window.document.visibilityState === 'hidden') {
|
|
||||||
clearTimeout(timeoutIdIdleA)
|
|
||||||
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
|
|
||||||
} else if (!engineCommandManager.engineConnection?.isReady()) {
|
|
||||||
clearTimeout(timeoutIdIdleA)
|
|
||||||
setStreamState(StreamState.Resuming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Teardown everything if we go hidden or reconnect
|
|
||||||
if (IDLE) {
|
|
||||||
globalThis?.window?.document?.addEventListener(
|
|
||||||
'visibilitychange',
|
|
||||||
onVisibilityChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
|
|
||||||
|
|
||||||
const onAnyInput = () => {
|
|
||||||
if (streamState === StreamState.Playing) {
|
|
||||||
// Clear both timers
|
|
||||||
clearTimeout(timeoutIdIdleA)
|
|
||||||
clearTimeout(timeoutIdIdleB)
|
|
||||||
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
|
||||||
}
|
|
||||||
if (streamState === StreamState.Paused) {
|
|
||||||
setStreamState(StreamState.Resuming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IDLE) {
|
|
||||||
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
|
|
||||||
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
|
|
||||||
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
|
|
||||||
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
|
|
||||||
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IDLE) {
|
|
||||||
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a listener to execute code and play the stream
|
|
||||||
* on initial stream setup.
|
|
||||||
*/
|
|
||||||
engineCommandManager.addEventListener(
|
|
||||||
EngineCommandManagerEvents.SceneReady,
|
|
||||||
executeCodeAndPlayStream
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
engineCommandManager.removeEventListener(
|
|
||||||
EngineCommandManagerEvents.SceneReady,
|
|
||||||
executeCodeAndPlayStream
|
|
||||||
)
|
|
||||||
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
|
|
||||||
capture: true,
|
|
||||||
})
|
|
||||||
if (IDLE) {
|
|
||||||
clearTimeout(timeoutIdIdleA)
|
|
||||||
clearTimeout(timeoutIdIdleB)
|
|
||||||
|
|
||||||
globalThis?.window?.document?.removeEventListener(
|
|
||||||
'visibilitychange',
|
|
||||||
onVisibilityChange
|
|
||||||
)
|
|
||||||
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
|
|
||||||
globalThis?.window?.document?.removeEventListener(
|
|
||||||
'mousemove',
|
|
||||||
onAnyInput
|
|
||||||
)
|
|
||||||
globalThis?.window?.document?.removeEventListener(
|
|
||||||
'mousedown',
|
|
||||||
onAnyInput
|
|
||||||
)
|
|
||||||
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
|
|
||||||
globalThis?.window?.document?.removeEventListener(
|
|
||||||
'touchstart',
|
|
||||||
onAnyInput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [IDLE, streamState])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play the vid
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!kclManager.isExecuting) {
|
|
||||||
setTimeout(() => {
|
|
||||||
// execute in the next event loop
|
|
||||||
videoRef.current?.play().catch((e) => {
|
|
||||||
console.warn('Video playing was prevented', e, videoRef.current)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [kclManager.isExecuting])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
typeof window === 'undefined' ||
|
|
||||||
typeof RTCPeerConnection === 'undefined'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if (!videoRef.current) return
|
|
||||||
if (!mediaStream) return
|
|
||||||
|
|
||||||
// The browser complains if we try to load a new stream without pausing first.
|
|
||||||
// Do not immediately play the stream!
|
|
||||||
try {
|
|
||||||
videoRef.current.srcObject = mediaStream
|
|
||||||
videoRef.current.pause()
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Attempted to pause stream while play was still loading', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
send({
|
|
||||||
type: 'Set context',
|
|
||||||
data: {
|
|
||||||
videoElement: videoRef.current,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
}, [mediaStream])
|
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
|
||||||
// If we've got no stream or connection, don't do anything
|
|
||||||
if (!isNetworkOkay) return
|
|
||||||
if (!videoRef.current) return
|
|
||||||
// If we're in sketch mode, don't send a engine-side select event
|
|
||||||
if (state.matches('Sketch')) return
|
|
||||||
if (state.matches({ idle: 'showPlanes' })) return
|
|
||||||
// If we're mousing up from a camera drag, don't send a select event
|
|
||||||
if (sceneInfra.camControls.wasDragging === true) return
|
|
||||||
|
|
||||||
if (btnName(e.nativeEvent).left) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
sendSelectEventToEngine(e, videoRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 z-0"
|
|
||||||
id="stream"
|
|
||||||
data-testid="stream"
|
|
||||||
onClick={handleMouseUp}
|
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
|
||||||
onContextMenuCapture={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
muted
|
|
||||||
autoPlay
|
|
||||||
controls={false}
|
|
||||||
onPlay={() => setIsLoading(false)}
|
|
||||||
className="w-full cursor-pointer h-full"
|
|
||||||
disablePictureInPicture
|
|
||||||
id="video-stream"
|
|
||||||
/>
|
|
||||||
<ClientSideScene
|
|
||||||
cameraControls={settings.context.modeling.mouseControls.current}
|
|
||||||
/>
|
|
||||||
{(streamState === StreamState.Paused ||
|
|
||||||
streamState === StreamState.Resuming) && (
|
|
||||||
<div className="text-center absolute inset-0">
|
|
||||||
<div
|
|
||||||
className="flex flex-col items-center justify-center h-screen"
|
|
||||||
data-testid="paused"
|
|
||||||
>
|
|
||||||
<div className="border-primary border p-2 rounded-sm">
|
|
||||||
<svg
|
|
||||||
width="8"
|
|
||||||
height="12"
|
|
||||||
viewBox="0 0 8 12"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M2 12V0H0V12H2ZM8 12V0H6V12H8Z"
|
|
||||||
fill="var(--primary)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-base mt-2 text-primary bold">
|
|
||||||
{streamState === StreamState.Paused && 'Paused'}
|
|
||||||
{streamState === StreamState.Resuming && 'Resuming'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(!isNetworkOkay || isLoading) && (
|
|
||||||
<div className="text-center absolute inset-0">
|
|
||||||
<Loading>
|
|
||||||
{!isNetworkOkay && !isLoading ? (
|
|
||||||
<span data-testid="loading-stream">Stream disconnected...</span>
|
|
||||||
) : (
|
|
||||||
!isLoading && (
|
|
||||||
<span data-testid="loading-stream">Loading stream...</span>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Loading>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -88,10 +88,6 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
? [codeRef.range]
|
? [codeRef.range]
|
||||||
: [codeRef.range, consumedCodeRef.range]
|
: [codeRef.range, consumedCodeRef.range]
|
||||||
)
|
)
|
||||||
} else if (artifact?.type === 'plane') {
|
|
||||||
const codeRef = artifact.codeRef
|
|
||||||
if (err(codeRef)) return
|
|
||||||
editorManager.setHighlightRange([codeRef.range])
|
|
||||||
} else {
|
} else {
|
||||||
editorManager.setHighlightRange([[0, 0]])
|
editorManager.setHighlightRange([[0, 0]])
|
||||||
}
|
}
|
||||||
@ -190,42 +186,8 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const artifact =
|
|
||||||
engineCommandManager.artifactGraph.get(planeOrFaceId)
|
|
||||||
|
|
||||||
if (artifact?.type === 'plane') {
|
|
||||||
const planeInfo = await getFaceDetails(planeOrFaceId)
|
|
||||||
sceneInfra.modelingSend({
|
|
||||||
type: 'Select default plane',
|
|
||||||
data: {
|
|
||||||
type: 'offsetPlane',
|
|
||||||
zAxis: [
|
|
||||||
planeInfo.z_axis.x,
|
|
||||||
planeInfo.z_axis.y,
|
|
||||||
planeInfo.z_axis.z,
|
|
||||||
],
|
|
||||||
yAxis: [
|
|
||||||
planeInfo.y_axis.x,
|
|
||||||
planeInfo.y_axis.y,
|
|
||||||
planeInfo.y_axis.z,
|
|
||||||
],
|
|
||||||
position: [
|
|
||||||
planeInfo.origin.x,
|
|
||||||
planeInfo.origin.y,
|
|
||||||
planeInfo.origin.z,
|
|
||||||
].map((num) => num / sceneInfra._baseUnitMultiplier) as [
|
|
||||||
number,
|
|
||||||
number,
|
|
||||||
number
|
|
||||||
],
|
|
||||||
planeId: planeOrFaceId,
|
|
||||||
pathToNode: artifact.codeRef.pathToNode,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Artifact is likely an extrusion face
|
|
||||||
const faceId = planeOrFaceId
|
const faceId = planeOrFaceId
|
||||||
|
const artifact = engineCommandManager.artifactGraph.get(faceId)
|
||||||
const extrusion = getSweepFromSuspectedSweepSurface(
|
const extrusion = getSweepFromSuspectedSweepSurface(
|
||||||
faceId,
|
faceId,
|
||||||
engineCommandManager.artifactGraph
|
engineCommandManager.artifactGraph
|
||||||
|
237
src/hooks/useEngineStreamContext.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
|
||||||
|
import { MutableRefObject } from 'react'
|
||||||
|
import { setup, assign } from 'xstate'
|
||||||
|
import { createActorContext } from '@xstate/react'
|
||||||
|
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||||
|
import { trap } from 'lib/trap'
|
||||||
|
|
||||||
|
export enum EngineStreamState {
|
||||||
|
Off = 'off',
|
||||||
|
On = 'on',
|
||||||
|
Playing = 'playing',
|
||||||
|
Paused = 'paused',
|
||||||
|
Resuming = 'resuming',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EngineStreamTransition {
|
||||||
|
SetMediaStream = 'set-context',
|
||||||
|
Play = 'play',
|
||||||
|
Resume = 'resume',
|
||||||
|
Pause = 'pause',
|
||||||
|
StartOrReconfigureEngine = 'start-or-reconfigure-engine',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EngineStreamContext {
|
||||||
|
pool: string | null
|
||||||
|
authToken: string | null
|
||||||
|
mediaStream: MediaStream | null
|
||||||
|
videoRef: MutableRefObject<HTMLVideoElement | null>
|
||||||
|
canvasRef: MutableRefObject<HTMLCanvasElement | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDimensions(streamWidth: number, streamHeight: number) {
|
||||||
|
const factorOf = 4
|
||||||
|
const maxResolution = 2160
|
||||||
|
const ratio = Math.min(
|
||||||
|
Math.min(maxResolution / streamWidth, maxResolution / streamHeight),
|
||||||
|
1.0
|
||||||
|
)
|
||||||
|
const quadWidth = Math.round((streamWidth * ratio) / factorOf) * factorOf
|
||||||
|
const quadHeight = Math.round((streamHeight * ratio) / factorOf) * factorOf
|
||||||
|
return { width: quadWidth, height: quadHeight }
|
||||||
|
}
|
||||||
|
|
||||||
|
const engineStreamMachine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as EngineStreamContext,
|
||||||
|
input: {} as EngineStreamContext,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
[EngineStreamTransition.Play]({ context }, params: { zoomToFit: boolean }) {
|
||||||
|
const canvas = context.canvasRef.current
|
||||||
|
if (!canvas) return false
|
||||||
|
|
||||||
|
const video = context.videoRef.current
|
||||||
|
if (!video) return false
|
||||||
|
|
||||||
|
const mediaStream = context.mediaStream
|
||||||
|
if (!mediaStream) return false
|
||||||
|
|
||||||
|
video.style.display = 'block'
|
||||||
|
canvas.style.display = 'none'
|
||||||
|
|
||||||
|
video.srcObject = mediaStream
|
||||||
|
void sceneInfra.camControls
|
||||||
|
.restoreCameraPosition()
|
||||||
|
.then(() => video.play())
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('Video playing was prevented', e, video)
|
||||||
|
})
|
||||||
|
.then(() => kclManager.executeCode(params.zoomToFit))
|
||||||
|
.catch(trap)
|
||||||
|
},
|
||||||
|
[EngineStreamTransition.Pause]({ context }) {
|
||||||
|
const video = context.videoRef.current
|
||||||
|
if (!video) return
|
||||||
|
|
||||||
|
video.pause()
|
||||||
|
|
||||||
|
const canvas = context.canvasRef.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
canvas.width = video.videoWidth
|
||||||
|
canvas.height = video.videoHeight
|
||||||
|
canvas.style.width = video.videoWidth + 'px'
|
||||||
|
canvas.style.height = video.videoHeight + 'px'
|
||||||
|
canvas.style.display = 'block'
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Make sure we're on the next frame for no flickering between canvas
|
||||||
|
// and the video elements.
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
video.style.display = 'none'
|
||||||
|
|
||||||
|
// Destroy the media stream only. We will re-establish it. We could
|
||||||
|
// leave everything at pausing, preventing video decoders from running
|
||||||
|
// but we can do even better by significantly reducing network
|
||||||
|
// cards also.
|
||||||
|
context.mediaStream?.getVideoTracks()[0].stop()
|
||||||
|
video.srcObject = null
|
||||||
|
|
||||||
|
sceneInfra.camControls.old = {
|
||||||
|
camera: sceneInfra.camControls.camera.clone(),
|
||||||
|
target: sceneInfra.camControls.target.clone(),
|
||||||
|
}
|
||||||
|
|
||||||
|
engineCommandManager.tearDown({ idleMode: true })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async [EngineStreamTransition.StartOrReconfigureEngine]({
|
||||||
|
context,
|
||||||
|
event,
|
||||||
|
}) {
|
||||||
|
if (!context.authToken) return
|
||||||
|
|
||||||
|
const video = context.videoRef.current
|
||||||
|
if (!video) return
|
||||||
|
|
||||||
|
const { width, height } = getDimensions(
|
||||||
|
window.innerWidth,
|
||||||
|
window.innerHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
video.width = width
|
||||||
|
video.height = height
|
||||||
|
|
||||||
|
const settingsNext = {
|
||||||
|
// override the pool param (?pool=) to request a specific engine instance
|
||||||
|
// from a particular pool.
|
||||||
|
pool: context.pool,
|
||||||
|
...event.settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
engineCommandManager.settings = settingsNext
|
||||||
|
|
||||||
|
engineCommandManager.start({
|
||||||
|
setMediaStream: event.onMediaStream,
|
||||||
|
setIsStreamReady: (isStreamReady) =>
|
||||||
|
event.setAppState({ isStreamReady }),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
token: context.authToken,
|
||||||
|
settings: settingsNext,
|
||||||
|
makeDefaultPlanes: () => {
|
||||||
|
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||||
|
},
|
||||||
|
modifyGrid: (hidden: boolean) => {
|
||||||
|
return modifyGrid(kclManager.engineCommandManager, hidden)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
event.modelingMachineActorSend({
|
||||||
|
type: 'Set context',
|
||||||
|
data: {
|
||||||
|
streamDimensions: {
|
||||||
|
streamWidth: width,
|
||||||
|
streamHeight: height,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async [EngineStreamTransition.Resume]({ context, event }) {
|
||||||
|
// engineCommandManager.engineConnection?.reattachMediaStream()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).createMachine({
|
||||||
|
context: (initial) => initial.input,
|
||||||
|
initial: EngineStreamState.Off,
|
||||||
|
states: {
|
||||||
|
[EngineStreamState.Off]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
||||||
|
target: EngineStreamState.On,
|
||||||
|
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EngineStreamState.On]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.SetMediaStream]: {
|
||||||
|
target: EngineStreamState.On,
|
||||||
|
actions: [
|
||||||
|
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[EngineStreamTransition.Play]: {
|
||||||
|
target: EngineStreamState.Playing,
|
||||||
|
actions: [
|
||||||
|
{ type: EngineStreamTransition.Play, params: { zoomToFit: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EngineStreamState.Playing]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
||||||
|
target: EngineStreamState.Playing,
|
||||||
|
reenter: true,
|
||||||
|
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
|
||||||
|
},
|
||||||
|
[EngineStreamTransition.Pause]: {
|
||||||
|
target: EngineStreamState.Paused,
|
||||||
|
actions: [{ type: EngineStreamTransition.Pause }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EngineStreamState.Paused]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
||||||
|
target: EngineStreamState.Resuming,
|
||||||
|
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EngineStreamState.Resuming]: {
|
||||||
|
on: {
|
||||||
|
[EngineStreamTransition.SetMediaStream]: {
|
||||||
|
target: EngineStreamState.Resuming,
|
||||||
|
actions: [
|
||||||
|
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[EngineStreamTransition.Play]: {
|
||||||
|
target: EngineStreamState.Playing,
|
||||||
|
actions: [
|
||||||
|
{ type: EngineStreamTransition.Play, params: { zoomToFit: false } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default createActorContext(engineStreamMachine)
|
@ -2,7 +2,7 @@ import { executeAst, lintAst } from 'lang/langHelpers'
|
|||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
import { KCLError, kclErrorsToDiagnostics } from './errors'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { EngineCommandManager } from './std/engineConnection'
|
import { EngineCommandManager, CommandLogType } from './std/engineConnection'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
||||||
|
|
||||||
@ -290,15 +290,9 @@ export class KclManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.engineCommandManager.sendSceneCommand({
|
await sceneInfra.camControls.centerModelRelativeToPanes({
|
||||||
type: 'modeling_cmd_req',
|
zoomToFit: true,
|
||||||
cmd_id: uuidv4(),
|
zoomObjectId,
|
||||||
cmd: {
|
|
||||||
type: 'zoom_to_fit',
|
|
||||||
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
|
|
||||||
padding: 0.1, // padding around the objects
|
|
||||||
animated: false, // don't animate the zoom for now
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -328,7 +322,7 @@ export class KclManager {
|
|||||||
this.ast = { ...ast }
|
this.ast = { ...ast }
|
||||||
this._executeCallback()
|
this._executeCallback()
|
||||||
this.engineCommandManager.addCommandLog({
|
this.engineCommandManager.addCommandLog({
|
||||||
type: 'execution-done',
|
type: CommandLogType.ExecutionDone,
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -6,17 +6,14 @@ import { isDesktop } from 'lib/isDesktop'
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { editorManager } from 'lib/singletons'
|
import { editorManager } from 'lib/singletons'
|
||||||
import { Annotation, Transaction } from '@codemirror/state'
|
import { Annotation, Transaction } from '@codemirror/state'
|
||||||
import { EditorView, KeyBinding } from '@codemirror/view'
|
import { KeyBinding } from '@codemirror/view'
|
||||||
import { recast, Program } from 'lang/wasm'
|
import { recast, Program } from 'lang/wasm'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { Compartment } from '@codemirror/state'
|
|
||||||
import { history } from '@codemirror/commands'
|
|
||||||
|
|
||||||
const PERSIST_CODE_KEY = 'persistCode'
|
const PERSIST_CODE_KEY = 'persistCode'
|
||||||
|
|
||||||
const codeManagerUpdateAnnotation = Annotation.define<boolean>()
|
const codeManagerUpdateAnnotation = Annotation.define<boolean>()
|
||||||
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(true)
|
export const codeManagerUpdateEvent = codeManagerUpdateAnnotation.of(true)
|
||||||
export const codeManagerHistoryCompartment = new Compartment()
|
|
||||||
|
|
||||||
export default class CodeManager {
|
export default class CodeManager {
|
||||||
private _code: string = bracket
|
private _code: string = bracket
|
||||||
@ -93,12 +90,9 @@ export default class CodeManager {
|
|||||||
/**
|
/**
|
||||||
* Update the code in the editor.
|
* Update the code in the editor.
|
||||||
*/
|
*/
|
||||||
updateCodeEditor(code: string, clearHistory?: boolean): void {
|
updateCodeEditor(code: string): void {
|
||||||
this.code = code
|
this.code = code
|
||||||
if (editorManager.editorView) {
|
if (editorManager.editorView) {
|
||||||
if (clearHistory) {
|
|
||||||
clearCodeMirrorHistory(editorManager.editorView)
|
|
||||||
}
|
|
||||||
editorManager.editorView.dispatch({
|
editorManager.editorView.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
from: 0,
|
from: 0,
|
||||||
@ -107,7 +101,7 @@ export default class CodeManager {
|
|||||||
},
|
},
|
||||||
annotations: [
|
annotations: [
|
||||||
codeManagerUpdateEvent,
|
codeManagerUpdateEvent,
|
||||||
Transaction.addToHistory.of(!clearHistory),
|
Transaction.addToHistory.of(true),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -116,11 +110,11 @@ export default class CodeManager {
|
|||||||
/**
|
/**
|
||||||
* Update the code, state, and the code the code mirror editor sees.
|
* Update the code, state, and the code the code mirror editor sees.
|
||||||
*/
|
*/
|
||||||
updateCodeStateEditor(code: string, clearHistory?: boolean): void {
|
updateCodeStateEditor(code: string): void {
|
||||||
if (this._code !== code) {
|
if (this._code !== code) {
|
||||||
this.code = code
|
this.code = code
|
||||||
this.#updateState(code)
|
this.#updateState(code)
|
||||||
this.updateCodeEditor(code, clearHistory)
|
this.updateCodeEditor(code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,17 +167,3 @@ function safeLSSetItem(key: string, value: string) {
|
|||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
localStorage?.setItem(key, value)
|
localStorage?.setItem(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearCodeMirrorHistory(view: EditorView) {
|
|
||||||
// Clear history
|
|
||||||
view.dispatch({
|
|
||||||
effects: [codeManagerHistoryCompartment.reconfigure([])],
|
|
||||||
annotations: [codeManagerUpdateEvent],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add history back
|
|
||||||
view.dispatch({
|
|
||||||
effects: [codeManagerHistoryCompartment.reconfigure([history()])],
|
|
||||||
annotations: [codeManagerUpdateEvent],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
Expr,
|
Expr,
|
||||||
Literal,
|
Literal,
|
||||||
LiteralValue,
|
|
||||||
PipeSubstitution,
|
PipeSubstitution,
|
||||||
Identifier,
|
Identifier,
|
||||||
ArrayExpression,
|
ArrayExpression,
|
||||||
@ -19,7 +18,6 @@ import {
|
|||||||
ProgramMemory,
|
ProgramMemory,
|
||||||
SourceRange,
|
SourceRange,
|
||||||
sketchFromKclValue,
|
sketchFromKclValue,
|
||||||
isPathToNodeNumber,
|
|
||||||
} from './wasm'
|
} from './wasm'
|
||||||
import {
|
import {
|
||||||
isNodeSafeToReplacePath,
|
isNodeSafeToReplacePath,
|
||||||
@ -527,60 +525,6 @@ export function sketchOnExtrudedFace(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Modify the AST to create a new sketch using the variable declaration
|
|
||||||
* of an offset plane. The new sketch just has to come after the offset
|
|
||||||
* plane declaration.
|
|
||||||
*/
|
|
||||||
export function sketchOnOffsetPlane(
|
|
||||||
node: Node<Program>,
|
|
||||||
offsetPathToNode: PathToNode
|
|
||||||
) {
|
|
||||||
let _node = { ...node }
|
|
||||||
|
|
||||||
// Find the offset plane declaration
|
|
||||||
const offsetPlaneDeclarator = getNodeFromPath<VariableDeclarator>(
|
|
||||||
_node,
|
|
||||||
offsetPathToNode,
|
|
||||||
'VariableDeclarator',
|
|
||||||
true
|
|
||||||
)
|
|
||||||
if (err(offsetPlaneDeclarator)) return offsetPlaneDeclarator
|
|
||||||
const { node: offsetPlaneNode } = offsetPlaneDeclarator
|
|
||||||
const offsetPlaneName = offsetPlaneNode.id.name
|
|
||||||
|
|
||||||
// Create a new sketch declaration
|
|
||||||
const newSketchName = findUniqueName(
|
|
||||||
node,
|
|
||||||
KCL_DEFAULT_CONSTANT_PREFIXES.SKETCH
|
|
||||||
)
|
|
||||||
const newSketch = createVariableDeclaration(
|
|
||||||
newSketchName,
|
|
||||||
createCallExpressionStdLib('startSketchOn', [
|
|
||||||
createIdentifier(offsetPlaneName),
|
|
||||||
]),
|
|
||||||
undefined,
|
|
||||||
'const'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Decide where to insert the new sketch declaration
|
|
||||||
const offsetIndex = offsetPathToNode[1][0]
|
|
||||||
|
|
||||||
if (!isPathToNodeNumber(offsetIndex)) {
|
|
||||||
return new Error('Expected offsetIndex to be a number')
|
|
||||||
}
|
|
||||||
// and insert it
|
|
||||||
_node.body.splice(offsetIndex + 1, 0, newSketch)
|
|
||||||
const newPathToNode = structuredClone(offsetPathToNode)
|
|
||||||
newPathToNode[1][0] = offsetIndex + 1
|
|
||||||
|
|
||||||
// Return the modified AST and the path to the new sketch declaration
|
|
||||||
return {
|
|
||||||
modifiedAst: _node,
|
|
||||||
pathToNode: newPathToNode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getLastIndex = (pathToNode: PathToNode): number =>
|
export const getLastIndex = (pathToNode: PathToNode): number =>
|
||||||
splitPathAtLastIndex(pathToNode).index
|
splitPathAtLastIndex(pathToNode).index
|
||||||
|
|
||||||
@ -629,7 +573,7 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
|
|||||||
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createLiteral(value: LiteralValue): Node<Literal> {
|
export function createLiteral(value: string | number): Node<Literal> {
|
||||||
return {
|
return {
|
||||||
type: 'Literal',
|
type: 'Literal',
|
||||||
start: 0,
|
start: 0,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { parse, recast, initPromise, PathToNode, Identifier } from './wasm'
|
import { parse, recast, initPromise, PathToNode } from './wasm'
|
||||||
import {
|
import {
|
||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
isNodeSafeToReplace,
|
isNodeSafeToReplace,
|
||||||
@ -10,7 +10,6 @@ import {
|
|||||||
hasSketchPipeBeenExtruded,
|
hasSketchPipeBeenExtruded,
|
||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
traverse,
|
traverse,
|
||||||
getNodeFromPath,
|
|
||||||
} from './queryAst'
|
} from './queryAst'
|
||||||
import { enginelessExecutor } from '../lib/testHelpers'
|
import { enginelessExecutor } from '../lib/testHelpers'
|
||||||
import {
|
import {
|
||||||
@ -267,86 +266,6 @@ describe('testing getNodePathFromSourceRange', () => {
|
|||||||
])
|
])
|
||||||
expect(selectWholeThing).toEqual(expected)
|
expect(selectWholeThing).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('finds the node in if-else condition', () => {
|
|
||||||
const code = `y = 0
|
|
||||||
x = if x > y {
|
|
||||||
x + 1
|
|
||||||
} else {
|
|
||||||
y
|
|
||||||
}`
|
|
||||||
const searchLn = `x > y`
|
|
||||||
const sourceIndex = code.indexOf(searchLn)
|
|
||||||
const ast = parse(code)
|
|
||||||
if (err(ast)) throw ast
|
|
||||||
|
|
||||||
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
|
||||||
expect(result).toEqual([
|
|
||||||
['body', ''],
|
|
||||||
[1, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', ''],
|
|
||||||
['cond', 'IfExpression'],
|
|
||||||
['left', 'BinaryExpression'],
|
|
||||||
])
|
|
||||||
const _node = getNodeFromPath<Identifier>(ast, result)
|
|
||||||
if (err(_node)) throw _node
|
|
||||||
expect(_node.node.type).toEqual('Identifier')
|
|
||||||
expect(_node.node.name).toEqual('x')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('finds the node in if-else then', () => {
|
|
||||||
const code = `y = 0
|
|
||||||
x = if x > y {
|
|
||||||
x + 1
|
|
||||||
} else {
|
|
||||||
y
|
|
||||||
}`
|
|
||||||
const searchLn = `x + 1`
|
|
||||||
const sourceIndex = code.indexOf(searchLn)
|
|
||||||
const ast = parse(code)
|
|
||||||
if (err(ast)) throw ast
|
|
||||||
|
|
||||||
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
|
||||||
expect(result).toEqual([
|
|
||||||
['body', ''],
|
|
||||||
[1, 'index'],
|
|
||||||
['declarations', 'VariableDeclaration'],
|
|
||||||
[0, 'index'],
|
|
||||||
['init', ''],
|
|
||||||
['then_val', 'IfExpression'],
|
|
||||||
['body', 'IfExpression'],
|
|
||||||
[0, 'index'],
|
|
||||||
['expression', 'ExpressionStatement'],
|
|
||||||
['left', 'BinaryExpression'],
|
|
||||||
])
|
|
||||||
const _node = getNodeFromPath<Identifier>(ast, result)
|
|
||||||
if (err(_node)) throw _node
|
|
||||||
expect(_node.node.type).toEqual('Identifier')
|
|
||||||
expect(_node.node.name).toEqual('x')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('finds the node in import statement item', () => {
|
|
||||||
const code = `import foo, bar as baz from 'thing.kcl'`
|
|
||||||
const searchLn = `bar`
|
|
||||||
const sourceIndex = code.indexOf(searchLn)
|
|
||||||
const ast = parse(code)
|
|
||||||
if (err(ast)) throw ast
|
|
||||||
|
|
||||||
const result = getNodePathFromSourceRange(ast, [sourceIndex, sourceIndex])
|
|
||||||
expect(result).toEqual([
|
|
||||||
['body', ''],
|
|
||||||
[0, 'index'],
|
|
||||||
['items', 'ImportStatement'],
|
|
||||||
[1, 'index'],
|
|
||||||
['name', 'ImportItem'],
|
|
||||||
])
|
|
||||||
const _node = getNodeFromPath<Identifier>(ast, result)
|
|
||||||
if (err(_node)) throw _node
|
|
||||||
expect(_node.node.type).toEqual('Identifier')
|
|
||||||
expect(_node.node.name).toEqual('bar')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('testing doesPipeHave', () => {
|
describe('testing doesPipeHave', () => {
|
||||||
|
@ -317,62 +317,6 @@ function moreNodePathFromSourceRange(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_node.type === 'PipeSubstitution' && isInRange) return path
|
if (_node.type === 'PipeSubstitution' && isInRange) return path
|
||||||
|
|
||||||
if (_node.type === 'IfExpression' && isInRange) {
|
|
||||||
const { cond, then_val, else_ifs, final_else } = _node
|
|
||||||
if (cond.start <= start && cond.end >= end) {
|
|
||||||
path.push(['cond', 'IfExpression'])
|
|
||||||
return moreNodePathFromSourceRange(cond, sourceRange, path)
|
|
||||||
}
|
|
||||||
if (then_val.start <= start && then_val.end >= end) {
|
|
||||||
path.push(['then_val', 'IfExpression'])
|
|
||||||
path.push(['body', 'IfExpression'])
|
|
||||||
return getNodePathFromSourceRange(then_val, sourceRange, path)
|
|
||||||
}
|
|
||||||
for (let i = 0; i < else_ifs.length; i++) {
|
|
||||||
const else_if = else_ifs[i]
|
|
||||||
if (else_if.start <= start && else_if.end >= end) {
|
|
||||||
path.push(['else_ifs', 'IfExpression'])
|
|
||||||
path.push([i, 'index'])
|
|
||||||
const { cond, then_val } = else_if
|
|
||||||
if (cond.start <= start && cond.end >= end) {
|
|
||||||
path.push(['cond', 'IfExpression'])
|
|
||||||
return moreNodePathFromSourceRange(cond, sourceRange, path)
|
|
||||||
}
|
|
||||||
path.push(['then_val', 'IfExpression'])
|
|
||||||
path.push(['body', 'IfExpression'])
|
|
||||||
return getNodePathFromSourceRange(then_val, sourceRange, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (final_else.start <= start && final_else.end >= end) {
|
|
||||||
path.push(['final_else', 'IfExpression'])
|
|
||||||
path.push(['body', 'IfExpression'])
|
|
||||||
return getNodePathFromSourceRange(final_else, sourceRange, path)
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_node.type === 'ImportStatement' && isInRange) {
|
|
||||||
const { items } = _node
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i]
|
|
||||||
if (item.start <= start && item.end >= end) {
|
|
||||||
path.push(['items', 'ImportStatement'])
|
|
||||||
path.push([i, 'index'])
|
|
||||||
if (item.name.start <= start && item.name.end >= end) {
|
|
||||||
path.push(['name', 'ImportItem'])
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (item.alias && item.alias.start <= start && item.alias.end >= end) {
|
|
||||||
path.push(['alias', 'ImportItem'])
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('not implemented: ' + node.type)
|
console.error('not implemented: ' + node.type)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
@ -98,22 +98,12 @@ sketch004 = startSketchOn(extrude003, seg02)
|
|||||||
|> close(%)
|
|> close(%)
|
||||||
extrude004 = extrude(3, sketch004)
|
extrude004 = extrude(3, sketch004)
|
||||||
`
|
`
|
||||||
const exampleCodeOffsetPlanes = `
|
|
||||||
offsetPlane001 = offsetPlane("XY", 20)
|
|
||||||
offsetPlane002 = offsetPlane("XZ", -50)
|
|
||||||
offsetPlane003 = offsetPlane("YZ", 10)
|
|
||||||
|
|
||||||
sketch002 = startSketchOn(offsetPlane001)
|
|
||||||
|> startProfileAt([0, 0], %)
|
|
||||||
|> line([6.78, 15.01], %)
|
|
||||||
`
|
|
||||||
|
|
||||||
// add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
|
// add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
|
||||||
const codeToWriteCacheFor = {
|
const codeToWriteCacheFor = {
|
||||||
exampleCode1,
|
exampleCode1,
|
||||||
sketchOnFaceOnFaceEtc,
|
sketchOnFaceOnFaceEtc,
|
||||||
exampleCodeNo3D,
|
exampleCodeNo3D,
|
||||||
exampleCodeOffsetPlanes,
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
type CodeKey = keyof typeof codeToWriteCacheFor
|
type CodeKey = keyof typeof codeToWriteCacheFor
|
||||||
@ -175,52 +165,6 @@ afterAll(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('testing createArtifactGraph', () => {
|
describe('testing createArtifactGraph', () => {
|
||||||
describe('code with offset planes and a sketch:', () => {
|
|
||||||
let ast: Program
|
|
||||||
let theMap: ReturnType<typeof createArtifactGraph>
|
|
||||||
|
|
||||||
it('setup', () => {
|
|
||||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
|
||||||
const {
|
|
||||||
orderedCommands,
|
|
||||||
responseMap,
|
|
||||||
ast: _ast,
|
|
||||||
} = getCommands('exampleCodeOffsetPlanes')
|
|
||||||
ast = _ast
|
|
||||||
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`there should be one sketch`, () => {
|
|
||||||
const sketches = [...filterArtifacts({ types: ['path'] }, theMap)].map(
|
|
||||||
(path) => expandPath(path[1], theMap)
|
|
||||||
)
|
|
||||||
expect(sketches).toHaveLength(1)
|
|
||||||
sketches.forEach((path) => {
|
|
||||||
if (err(path)) throw path
|
|
||||||
expect(path.type).toBe('path')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`there should be three offsetPlanes`, () => {
|
|
||||||
const offsetPlanes = [
|
|
||||||
...filterArtifacts({ types: ['plane'] }, theMap),
|
|
||||||
].map((plane) => expandPlane(plane[1], theMap))
|
|
||||||
expect(offsetPlanes).toHaveLength(3)
|
|
||||||
offsetPlanes.forEach((path) => {
|
|
||||||
expect(path.type).toBe('plane')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it(`Only one offset plane should have a path`, () => {
|
|
||||||
const offsetPlanes = [
|
|
||||||
...filterArtifacts({ types: ['plane'] }, theMap),
|
|
||||||
].map((plane) => expandPlane(plane[1], theMap))
|
|
||||||
const offsetPlaneWithPaths = offsetPlanes.filter(
|
|
||||||
(plane) => plane.paths.length
|
|
||||||
)
|
|
||||||
expect(offsetPlaneWithPaths).toHaveLength(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
describe('code with an extrusion, fillet and sketch of face:', () => {
|
describe('code with an extrusion, fillet and sketch of face:', () => {
|
||||||
let ast: Program
|
let ast: Program
|
||||||
let theMap: ReturnType<typeof createArtifactGraph>
|
let theMap: ReturnType<typeof createArtifactGraph>
|
||||||
|
@ -249,20 +249,7 @@ export function getArtifactsToUpdate({
|
|||||||
const cmd = command.cmd
|
const cmd = command.cmd
|
||||||
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
|
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
|
||||||
if (!response) return returnArr
|
if (!response) return returnArr
|
||||||
if (cmd.type === 'make_plane' && range[1] !== 0) {
|
if (cmd.type === 'enable_sketch_mode') {
|
||||||
// If we're calling `make_plane` and the code range doesn't end at `0`
|
|
||||||
// it's not a default plane, but a custom one from the offsetPlane standard library function
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
artifact: {
|
|
||||||
type: 'plane',
|
|
||||||
pathIds: [],
|
|
||||||
codeRef: { range, pathToNode },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
} else if (cmd.type === 'enable_sketch_mode') {
|
|
||||||
const plane = getArtifact(currentPlaneId)
|
const plane = getArtifact(currentPlaneId)
|
||||||
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
|
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
|
||||||
const codeRef =
|
const codeRef =
|
||||||
|
@ -406,13 +406,14 @@ class EngineConnection extends EventTarget {
|
|||||||
default:
|
default:
|
||||||
if (this.isConnecting()) break
|
if (this.isConnecting()) break
|
||||||
// Means we never could do an initial connection. Reconnect everything.
|
// Means we never could do an initial connection. Reconnect everything.
|
||||||
if (!this.pingPongSpan.ping) this.connect().catch(reportRejection)
|
if (!this.pingPongSpan.ping)
|
||||||
|
this.connect({ reconnect: false }).catch(reportRejection)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, pingIntervalMs)
|
}, pingIntervalMs)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.connect()
|
this.connect({ reconnect: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
// SHOULD ONLY BE USED FOR VITESTS
|
// SHOULD ONLY BE USED FOR VITESTS
|
||||||
@ -523,7 +524,9 @@ class EngineConnection extends EventTarget {
|
|||||||
this.idleMode = opts?.idleMode ?? false
|
this.idleMode = opts?.idleMode ?? false
|
||||||
clearInterval(this.pingIntervalId)
|
clearInterval(this.pingIntervalId)
|
||||||
|
|
||||||
if (opts?.idleMode) {
|
this.disconnectAll()
|
||||||
|
|
||||||
|
if (this.idleMode) {
|
||||||
this.state = {
|
this.state = {
|
||||||
type: EngineConnectionStateType.Disconnecting,
|
type: EngineConnectionStateType.Disconnecting,
|
||||||
value: {
|
value: {
|
||||||
@ -542,8 +545,6 @@ class EngineConnection extends EventTarget {
|
|||||||
type: DisconnectingType.Quit,
|
type: DisconnectingType.Quit,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
this.disconnectAll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -553,7 +554,7 @@ class EngineConnection extends EventTarget {
|
|||||||
* This will attempt the full handshake, and retry if the connection
|
* This will attempt the full handshake, and retry if the connection
|
||||||
* did not establish.
|
* did not establish.
|
||||||
*/
|
*/
|
||||||
connect(reconnecting?: boolean): Promise<void> {
|
connect(args: { reconnect: boolean }): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (this.isConnecting() || this.isReady()) {
|
if (this.isConnecting() || this.isReady()) {
|
||||||
return
|
return
|
||||||
@ -1165,7 +1166,7 @@ class EngineConnection extends EventTarget {
|
|||||||
this.websocket.addEventListener('message', this.onWebSocketMessage)
|
this.websocket.addEventListener('message', this.onWebSocketMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reconnecting) {
|
if (args.reconnect) {
|
||||||
createWebSocketConnection()
|
createWebSocketConnection()
|
||||||
} else {
|
} else {
|
||||||
this.onNetworkStatusReady = () => {
|
this.onNetworkStatusReady = () => {
|
||||||
@ -1178,6 +1179,32 @@ class EngineConnection extends EventTarget {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reattachMediaStream() {
|
||||||
|
return this.pc
|
||||||
|
?.createOffer({ iceRestart: true })
|
||||||
|
.then((offer: RTCSessionDescriptionInit) => {
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.SetLocalDescription,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return this.pc?.setLocalDescription(offer).then(() => {
|
||||||
|
this.send({
|
||||||
|
type: 'sdp_offer',
|
||||||
|
offer: offer as Models['RtcSessionDescription_type'],
|
||||||
|
})
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Connecting,
|
||||||
|
value: {
|
||||||
|
type: ConnectingType.OfferedSdp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Do not change this back to an object or any, we should only be sending the
|
// Do not change this back to an object or any, we should only be sending the
|
||||||
// WebSocketRequest type!
|
// WebSocketRequest type!
|
||||||
unreliableSend(message: Models['WebSocketRequest_type']) {
|
unreliableSend(message: Models['WebSocketRequest_type']) {
|
||||||
@ -1229,8 +1256,17 @@ class EngineConnection extends EventTarget {
|
|||||||
this.websocket?.readyState === 3
|
this.websocket?.readyState === 3
|
||||||
|
|
||||||
if (closedPc && closedUDC && closedWS) {
|
if (closedPc && closedUDC && closedWS) {
|
||||||
// Do not notify the rest of the program that we have cut off anything.
|
if (!this.idleMode) {
|
||||||
this.state = { type: EngineConnectionStateType.Disconnected }
|
// Do not notify the rest of the program that we have cut off anything.
|
||||||
|
this.state = { type: EngineConnectionStateType.Disconnected }
|
||||||
|
} else {
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnecting,
|
||||||
|
value: {
|
||||||
|
type: DisconnectingType.Pause,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1255,27 +1291,40 @@ export interface Subscription<T extends ModelTypes> {
|
|||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CommandLogType {
|
||||||
|
SendModeling = 'send-modeling',
|
||||||
|
SendScene = 'send-scene',
|
||||||
|
ReceiveReliable = 'receive-reliable',
|
||||||
|
ExecutionDone = 'execution-done',
|
||||||
|
ExportDone = 'export-done',
|
||||||
|
SetDefaultSystemProperties = 'set_default_system_properties',
|
||||||
|
}
|
||||||
|
|
||||||
export type CommandLog =
|
export type CommandLog =
|
||||||
| {
|
| {
|
||||||
type: 'send-modeling'
|
type: CommandLogType.SendModeling
|
||||||
data: EngineCommand
|
data: EngineCommand
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'send-scene'
|
type: CommandLogType.SendScene
|
||||||
data: EngineCommand
|
data: EngineCommand
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'receive-reliable'
|
type: CommandLogType.ReceiveReliable
|
||||||
data: OkWebSocketResponseData
|
data: OkWebSocketResponseData
|
||||||
id: string
|
id: string
|
||||||
cmd_type?: string
|
cmd_type?: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'execution-done'
|
type: CommandLogType.ExecutionDone
|
||||||
data: null
|
data: null
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'export-done'
|
type: CommandLogType.ExportDone
|
||||||
|
data: null
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: CommandLogType.SetDefaultSystemProperties
|
||||||
data: null
|
data: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1631,11 +1680,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
|
|
||||||
switch (this.exportInfo.intent) {
|
switch (this.exportInfo.intent) {
|
||||||
case ExportIntent.Save: {
|
case ExportIntent.Save: {
|
||||||
exportSave({
|
exportSave(event.data, this.pendingExport.toastId).then(() => {
|
||||||
data: event.data,
|
|
||||||
fileName: this.exportInfo.name,
|
|
||||||
toastId: this.pendingExport.toastId,
|
|
||||||
}).then(() => {
|
|
||||||
this.pendingExport?.resolve(null)
|
this.pendingExport?.resolve(null)
|
||||||
}, this.pendingExport?.reject)
|
}, this.pendingExport?.reject)
|
||||||
break
|
break
|
||||||
@ -1690,7 +1735,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
message.request_id
|
message.request_id
|
||||||
) {
|
) {
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: 'receive-reliable',
|
type: CommandLogType.ReceiveReliable,
|
||||||
data: message.resp,
|
data: message.resp,
|
||||||
id: message?.request_id || '',
|
id: message?.request_id || '',
|
||||||
cmd_type: pending?.command?.cmd?.type,
|
cmd_type: pending?.command?.cmd?.type,
|
||||||
@ -1724,7 +1769,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
if (!command) return
|
if (!command) return
|
||||||
if (command.type === 'modeling_cmd_req')
|
if (command.type === 'modeling_cmd_req')
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: 'receive-reliable',
|
type: CommandLogType.ReceiveReliable,
|
||||||
data: {
|
data: {
|
||||||
type: 'modeling',
|
type: 'modeling',
|
||||||
data: {
|
data: {
|
||||||
@ -1766,7 +1811,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.engineConnection?.connect()
|
this.engineConnection?.connect({ reconnect: false })
|
||||||
}
|
}
|
||||||
this.engineConnection.addEventListener(
|
this.engineConnection.addEventListener(
|
||||||
EngineConnectionEvents.ConnectionStarted,
|
EngineConnectionEvents.ConnectionStarted,
|
||||||
@ -1828,6 +1873,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.engineConnection?.tearDown(opts)
|
this.engineConnection?.tearDown(opts)
|
||||||
|
this.engineConnection = undefined
|
||||||
|
|
||||||
// Our window.tearDown assignment causes this case to happen which is
|
// Our window.tearDown assignment causes this case to happen which is
|
||||||
// only really for tests.
|
// only really for tests.
|
||||||
@ -1835,6 +1881,8 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
} else if (this.engineCommandManager?.engineConnection) {
|
} else if (this.engineCommandManager?.engineConnection) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.engineCommandManager?.engineConnection?.tearDown(opts)
|
this.engineCommandManager?.engineConnection?.tearDown(opts)
|
||||||
|
// @ts-ignore
|
||||||
|
this.engineCommandManager.engineConnection = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async startNewSession() {
|
async startNewSession() {
|
||||||
@ -1933,7 +1981,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
) {
|
) {
|
||||||
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
|
// highlight_set_entity, mouse_move and camera_drag_move are sent over the unreliable channel and are too noisy
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: 'send-scene',
|
type: CommandLogType.SendScene,
|
||||||
data: command,
|
data: command,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1992,7 +2040,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
toastId,
|
toastId,
|
||||||
resolve: (passThrough) => {
|
resolve: (passThrough) => {
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: 'export-done',
|
type: CommandLogType.ExportDone,
|
||||||
data: null,
|
data: null,
|
||||||
})
|
})
|
||||||
resolve(passThrough)
|
resolve(passThrough)
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import {
|
import { Program, PathToNode } from './wasm'
|
||||||
Program,
|
|
||||||
PathToNode,
|
|
||||||
CallExpression,
|
|
||||||
Literal,
|
|
||||||
ArrayExpression,
|
|
||||||
BinaryExpression,
|
|
||||||
} from './wasm'
|
|
||||||
import { getNodeFromPath } from './queryAst'
|
import { getNodeFromPath } from './queryAst'
|
||||||
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
|
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
|
||||||
import { isOverlap } from 'lib/utils'
|
import { isOverlap } from 'lib/utils'
|
||||||
@ -91,19 +84,3 @@ export function isCursorInSketchCommandRange(
|
|||||||
([, artifact]) => artifact.type === 'path'
|
([, artifact]) => artifact.type === 'path'
|
||||||
)?.[0] || false
|
)?.[0] || false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCallExpression(e: any): e is CallExpression {
|
|
||||||
return e && e.type === 'CallExpression'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isArrayExpression(e: any): e is ArrayExpression {
|
|
||||||
return e && e.type === 'ArrayExpression'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLiteral(e: any): e is Literal {
|
|
||||||
return e && e.type === 'Literal'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isBinaryExpression(e: any): e is BinaryExpression {
|
|
||||||
return e && e.type === 'BinaryExpression'
|
|
||||||
}
|
|
||||||
|
@ -62,7 +62,6 @@ export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression'
|
|||||||
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
|
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
|
||||||
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
|
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
|
||||||
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
|
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
|
||||||
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
|
|
||||||
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
|
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
|
||||||
|
|
||||||
export type SyntaxType =
|
export type SyntaxType =
|
||||||
@ -82,7 +81,6 @@ export type SyntaxType =
|
|||||||
| 'PipeExpression'
|
| 'PipeExpression'
|
||||||
| 'PipeSubstitution'
|
| 'PipeSubstitution'
|
||||||
| 'Literal'
|
| 'Literal'
|
||||||
| 'LiteralValue'
|
|
||||||
| 'NonCodeNode'
|
| 'NonCodeNode'
|
||||||
| 'UnaryExpression'
|
| 'UnaryExpression'
|
||||||
|
|
||||||
@ -144,12 +142,6 @@ export const parse = (code: string | Error): Node<Program> | Error => {
|
|||||||
|
|
||||||
export type PathToNode = [string | number, string][]
|
export type PathToNode = [string | number, string][]
|
||||||
|
|
||||||
export const isPathToNodeNumber = (
|
|
||||||
pathToNode: string | number
|
|
||||||
): pathToNode is number => {
|
|
||||||
return typeof pathToNode === 'number'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExecState {
|
export interface ExecState {
|
||||||
memory: ProgramMemory
|
memory: ProgramMemory
|
||||||
idGenerator: IdGenerator
|
idGenerator: IdGenerator
|
||||||
|
@ -68,16 +68,7 @@ const save_ = async (file: ModelingAppFile, toastId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Saves files locally from an export call.
|
// Saves files locally from an export call.
|
||||||
// We override the file's name with one passed in from the client side.
|
export async function exportSave(data: ArrayBuffer, toastId: string) {
|
||||||
export async function exportSave({
|
|
||||||
data,
|
|
||||||
fileName,
|
|
||||||
toastId,
|
|
||||||
}: {
|
|
||||||
data: ArrayBuffer
|
|
||||||
fileName: string
|
|
||||||
toastId: string
|
|
||||||
}) {
|
|
||||||
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
|
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
|
||||||
let uintArray = new Uint8Array(data)
|
let uintArray = new Uint8Array(data)
|
||||||
|
|
||||||
@ -89,10 +80,9 @@ export async function exportSave({
|
|||||||
zip.file(file.name, new Uint8Array(file.contents), { binary: true })
|
zip.file(file.name, new Uint8Array(file.contents), { binary: true })
|
||||||
}
|
}
|
||||||
return zip.generateAsync({ type: 'array' }).then((contents) => {
|
return zip.generateAsync({ type: 'array' }).then((contents) => {
|
||||||
return save_({ name: `${fileName || 'output'}.zip`, contents }, toastId)
|
return save_({ name: 'output.zip', contents }, toastId)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
files[0].name = fileName || files[0].name
|
|
||||||
return save_(files[0], toastId)
|
return save_(files[0], toastId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,16 +9,8 @@ import {
|
|||||||
createUnaryExpression,
|
createUnaryExpression,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm'
|
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm'
|
||||||
import { roundOff } from 'lib/utils'
|
|
||||||
import {
|
|
||||||
isCallExpression,
|
|
||||||
isArrayExpression,
|
|
||||||
isLiteral,
|
|
||||||
isBinaryExpression,
|
|
||||||
} from 'lang/util'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It does not create the startSketchOn and it does not create the startProfileAt.
|
|
||||||
* Returns AST expressions for this KCL code:
|
* Returns AST expressions for this KCL code:
|
||||||
* const yo = startSketchOn('XY')
|
* const yo = startSketchOn('XY')
|
||||||
* |> startProfileAt([0, 0], %)
|
* |> startProfileAt([0, 0], %)
|
||||||
@ -100,69 +92,3 @@ export function updateRectangleSketch(
|
|||||||
createLiteral(Math.abs(y)), // This will be the height of the rectangle
|
createLiteral(Math.abs(y)), // This will be the height of the rectangle
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mutates the pipeExpression to update the center rectangle sketch
|
|
||||||
* @param pipeExpression
|
|
||||||
* @param x
|
|
||||||
* @param y
|
|
||||||
* @param tag
|
|
||||||
*/
|
|
||||||
export function updateCenterRectangleSketch(
|
|
||||||
pipeExpression: PipeExpression,
|
|
||||||
deltaX: number,
|
|
||||||
deltaY: number,
|
|
||||||
tag: string,
|
|
||||||
originX: number,
|
|
||||||
originY: number
|
|
||||||
) {
|
|
||||||
let startX = originX - Math.abs(deltaX)
|
|
||||||
let startY = originY - Math.abs(deltaY)
|
|
||||||
|
|
||||||
// pipeExpression.body[1] is startProfileAt
|
|
||||||
let callExpression = pipeExpression.body[1]
|
|
||||||
if (isCallExpression(callExpression)) {
|
|
||||||
const arrayExpression = callExpression.arguments[0]
|
|
||||||
if (isArrayExpression(arrayExpression)) {
|
|
||||||
callExpression.arguments[0] = createArrayExpression([
|
|
||||||
createLiteral(roundOff(startX)),
|
|
||||||
createLiteral(roundOff(startY)),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const twoX = deltaX * 2
|
|
||||||
const twoY = deltaY * 2
|
|
||||||
|
|
||||||
callExpression = pipeExpression.body[2]
|
|
||||||
if (isCallExpression(callExpression)) {
|
|
||||||
const arrayExpression = callExpression.arguments[0]
|
|
||||||
if (isArrayExpression(arrayExpression)) {
|
|
||||||
const literal = arrayExpression.elements[0]
|
|
||||||
if (isLiteral(literal)) {
|
|
||||||
callExpression.arguments[0] = createArrayExpression([
|
|
||||||
createLiteral(literal.value),
|
|
||||||
createLiteral(Math.abs(twoX)),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callExpression = pipeExpression.body[3]
|
|
||||||
if (isCallExpression(callExpression)) {
|
|
||||||
const arrayExpression = callExpression.arguments[0]
|
|
||||||
if (isArrayExpression(arrayExpression)) {
|
|
||||||
const binaryExpression = arrayExpression.elements[0]
|
|
||||||
if (isBinaryExpression(binaryExpression)) {
|
|
||||||
callExpression.arguments[0] = createArrayExpression([
|
|
||||||
createBinaryExpression([
|
|
||||||
createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
|
|
||||||
binaryExpression.operator,
|
|
||||||
createLiteral(90),
|
|
||||||
]), // 90 offset from the previous line
|
|
||||||
createLiteral(Math.abs(twoY)), // This will be the height of the rectangle
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -124,9 +124,7 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
// We explicitly do not write to the file here since we are loading from
|
// We explicitly do not write to the file here since we are loading from
|
||||||
// the file system and not the editor.
|
// the file system and not the editor.
|
||||||
codeManager.updateCurrentFilePath(currentFilePath)
|
codeManager.updateCurrentFilePath(currentFilePath)
|
||||||
// We pass true on the end here to clear the code editor history.
|
codeManager.updateCodeStateEditor(code)
|
||||||
// This way undo and redo are not super weird when opening new files.
|
|
||||||
codeManager.updateCodeStateEditor(code, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the file system manager to the project path
|
// Set the file system manager to the project path
|
||||||
@ -147,12 +145,6 @@ export const fileLoader: LoaderFunction = async (
|
|||||||
? await getProjectInfo(projectPath)
|
? await getProjectInfo(projectPath)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
console.log('maybeProjectInfo', {
|
|
||||||
maybeProjectInfo,
|
|
||||||
defaultProjectData,
|
|
||||||
projectPathData,
|
|
||||||
})
|
|
||||||
|
|
||||||
const projectData: IndexLoaderData = {
|
const projectData: IndexLoaderData = {
|
||||||
code,
|
code,
|
||||||
project: maybeProjectInfo ?? defaultProjectData,
|
project: maybeProjectInfo ?? defaultProjectData,
|
||||||
|
@ -118,6 +118,8 @@ export class Setting<T = unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MS_IN_MINUTE = 1000 * 60
|
||||||
|
|
||||||
export function createSettings() {
|
export function createSettings() {
|
||||||
return {
|
return {
|
||||||
/** Settings that affect the behavior of the entire app,
|
/** Settings that affect the behavior of the entire app,
|
||||||
@ -181,13 +183,58 @@ export function createSettings() {
|
|||||||
/**
|
/**
|
||||||
* Stream resource saving behavior toggle
|
* Stream resource saving behavior toggle
|
||||||
*/
|
*/
|
||||||
streamIdleMode: new Setting<boolean>({
|
streamIdleMode: new Setting<number | undefined>({
|
||||||
defaultValue: false,
|
defaultValue: undefined,
|
||||||
description: 'Toggle stream idling, saving bandwidth and battery',
|
description: 'Toggle stream idling, saving bandwidth and battery',
|
||||||
validate: (v) => typeof v === 'boolean',
|
validate: (v) =>
|
||||||
commandConfig: {
|
v === undefined ||
|
||||||
inputType: 'boolean',
|
(typeof v === 'number' &&
|
||||||
},
|
v >= 1 * MS_IN_MINUTE &&
|
||||||
|
v <= 60 * MS_IN_MINUTE),
|
||||||
|
Component: ({ value, updateValue }) => (
|
||||||
|
<div className="flex item-center gap-4 px-2 m-0 py-0">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={value !== undefined}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateValue(
|
||||||
|
!e.currentTarget.checked ? undefined : 5 * MS_IN_MINUTE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="block w-4 h-4"
|
||||||
|
/>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col grow">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
onChange={(e) =>
|
||||||
|
updateValue(Number(e.currentTarget.value) * MS_IN_MINUTE)
|
||||||
|
}
|
||||||
|
disabled={value === undefined}
|
||||||
|
value={
|
||||||
|
value !== null && value !== undefined
|
||||||
|
? value / MS_IN_MINUTE
|
||||||
|
: 5
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
max={60}
|
||||||
|
step={1}
|
||||||
|
className="block flex-1"
|
||||||
|
/>
|
||||||
|
{value !== undefined && value !== null && (
|
||||||
|
<div>
|
||||||
|
{value / MS_IN_MINUTE === 60
|
||||||
|
? '1 hour'
|
||||||
|
: value / MS_IN_MINUTE === 1
|
||||||
|
? '1 minute'
|
||||||
|
: value / MS_IN_MINUTE + ' minutes'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
onboardingStatus: new Setting<string>({
|
onboardingStatus: new Setting<string>({
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
|
@ -24,6 +24,10 @@ import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration
|
|||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
import { DeepPartial } from 'lib/types'
|
import { DeepPartial } from 'lib/types'
|
||||||
|
|
||||||
|
type OmitNull<T> = T extends null ? undefined : T
|
||||||
|
const toUndefinedIfNull = (a: any): OmitNull<any> =>
|
||||||
|
a === null ? undefined : a
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert from a rust settings struct into the JS settings struct.
|
* Convert from a rust settings struct into the JS settings struct.
|
||||||
* We do this because the JS settings type has all the fancy shit
|
* We do this because the JS settings type has all the fancy shit
|
||||||
@ -40,7 +44,9 @@ export function configurationToSettingsPayload(
|
|||||||
: undefined,
|
: undefined,
|
||||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||||
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
|
streamIdleMode: toUndefinedIfNull(
|
||||||
|
configuration?.settings?.app?.stream_idle_mode
|
||||||
|
),
|
||||||
projectDirectory: configuration?.settings?.project?.directory,
|
projectDirectory: configuration?.settings?.project?.directory,
|
||||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||||
},
|
},
|
||||||
@ -79,7 +85,9 @@ export function projectConfigurationToSettingsPayload(
|
|||||||
: undefined,
|
: undefined,
|
||||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
||||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
||||||
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
|
streamIdleMode: toUndefinedIfNull(
|
||||||
|
configuration?.settings?.app?.stream_idle_mode
|
||||||
|
),
|
||||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
||||||
},
|
},
|
||||||
modeling: {
|
modeling: {
|
||||||
|
@ -10,8 +10,14 @@ export const codeManager = new CodeManager()
|
|||||||
|
|
||||||
export const engineCommandManager = new EngineCommandManager()
|
export const engineCommandManager = new EngineCommandManager()
|
||||||
|
|
||||||
// Accessible for tests mostly
|
declare global {
|
||||||
// @ts-ignore
|
interface Window {
|
||||||
|
tearDown: typeof engineCommandManager.tearDown
|
||||||
|
sceneInfra: typeof sceneInfra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessible for tests
|
||||||
window.tearDown = engineCommandManager.tearDown
|
window.tearDown = engineCommandManager.tearDown
|
||||||
|
|
||||||
// This needs to be after codeManager is created.
|
// This needs to be after codeManager is created.
|
||||||
@ -21,7 +27,9 @@ engineCommandManager.kclManager = kclManager
|
|||||||
engineCommandManager.getAstCb = () => kclManager.ast
|
engineCommandManager.getAstCb = () => kclManager.ast
|
||||||
|
|
||||||
export const sceneInfra = new SceneInfra(engineCommandManager)
|
export const sceneInfra = new SceneInfra(engineCommandManager)
|
||||||
engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange
|
|
||||||
|
// Accessible for tests
|
||||||
|
window.sceneInfra = sceneInfra
|
||||||
|
|
||||||
export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
|
export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
|
||||||
|
|
||||||
|
3
src/lib/timings.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// 0.25s is the average visual reaction time for humans so we'll go a bit less
|
||||||
|
// so those exception people don't see.
|
||||||
|
export const REASONABLE_TIME_TO_REFRESH_STREAM_SIZE = 100
|
@ -407,9 +407,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
status: 'available',
|
status: 'available',
|
||||||
title: 'Center circle',
|
title: 'Center circle',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
state.matches('Sketch no face') ||
|
!canRectangleOrCircleTool(state.context) &&
|
||||||
(!canRectangleOrCircleTool(state.context) &&
|
!state.matches({ Sketch: 'Circle tool' }),
|
||||||
!state.matches({ Sketch: 'Circle tool' })),
|
|
||||||
isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
|
isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
|
||||||
hotkey: (state) =>
|
hotkey: (state) =>
|
||||||
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
|
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
|
||||||
@ -449,9 +448,8 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
icon: 'rectangle',
|
icon: 'rectangle',
|
||||||
status: 'available',
|
status: 'available',
|
||||||
disabled: (state) =>
|
disabled: (state) =>
|
||||||
state.matches('Sketch no face') ||
|
!canRectangleOrCircleTool(state.context) &&
|
||||||
(!canRectangleOrCircleTool(state.context) &&
|
!state.matches({ Sketch: 'Rectangle tool' }),
|
||||||
!state.matches({ Sketch: 'Rectangle tool' })),
|
|
||||||
title: 'Corner rectangle',
|
title: 'Corner rectangle',
|
||||||
hotkey: (state) =>
|
hotkey: (state) =>
|
||||||
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
|
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
|
||||||
@ -461,33 +459,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'center-rectangle',
|
id: 'center-rectangle',
|
||||||
onClick: ({ modelingState, modelingSend }) =>
|
onClick: () => console.error('Center rectangle not yet implemented'),
|
||||||
modelingSend({
|
icon: 'rectangle',
|
||||||
type: 'change tool',
|
status: 'unavailable',
|
||||||
data: {
|
|
||||||
tool: !modelingState.matches({
|
|
||||||
Sketch: 'Center Rectangle tool',
|
|
||||||
})
|
|
||||||
? 'center rectangle'
|
|
||||||
: 'none',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
icon: 'arc',
|
|
||||||
status: 'available',
|
|
||||||
disabled: (state) =>
|
|
||||||
state.matches('Sketch no face') ||
|
|
||||||
(!canRectangleOrCircleTool(state.context) &&
|
|
||||||
!state.matches({ Sketch: 'Center Rectangle tool' })),
|
|
||||||
title: 'Center rectangle',
|
title: 'Center rectangle',
|
||||||
hotkey: (state) =>
|
showTitle: false,
|
||||||
state.matches({ Sketch: 'Center Rectangle tool' })
|
|
||||||
? ['Esc', 'C']
|
|
||||||
: 'C',
|
|
||||||
description: 'Start drawing a rectangle from its center',
|
description: 'Start drawing a rectangle from its center',
|
||||||
links: [],
|
links: [],
|
||||||
isActive: (state) => {
|
|
||||||
return state.matches({ Sketch: 'Center Rectangle tool' })
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
@ -159,15 +159,6 @@ export type DefaultPlane = {
|
|||||||
yAxis: [number, number, number]
|
yAxis: [number, number, number]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OffsetPlane = {
|
|
||||||
type: 'offsetPlane'
|
|
||||||
position: [number, number, number]
|
|
||||||
planeId: string
|
|
||||||
pathToNode: PathToNode
|
|
||||||
zAxis: [number, number, number]
|
|
||||||
yAxis: [number, number, number]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SegmentOverlayPayload =
|
export type SegmentOverlayPayload =
|
||||||
| {
|
| {
|
||||||
type: 'set-one'
|
type: 'set-one'
|
||||||
@ -193,7 +184,6 @@ export type SketchTool =
|
|||||||
| 'line'
|
| 'line'
|
||||||
| 'tangentialArc'
|
| 'tangentialArc'
|
||||||
| 'rectangle'
|
| 'rectangle'
|
||||||
| 'center rectangle'
|
|
||||||
| 'circle'
|
| 'circle'
|
||||||
| 'none'
|
| 'none'
|
||||||
|
|
||||||
@ -207,7 +197,7 @@ export type ModelingMachineEvent =
|
|||||||
| { type: 'Sketch On Face' }
|
| { type: 'Sketch On Face' }
|
||||||
| {
|
| {
|
||||||
type: 'Select default plane'
|
type: 'Select default plane'
|
||||||
data: DefaultPlane | ExtrudeFacePlane | OffsetPlane
|
data: DefaultPlane | ExtrudeFacePlane
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'Set selection'
|
type: 'Set selection'
|
||||||
@ -248,10 +238,6 @@ export type ModelingMachineEvent =
|
|||||||
type: 'Add rectangle origin'
|
type: 'Add rectangle origin'
|
||||||
data: [x: number, y: number]
|
data: [x: number, y: number]
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
type: 'Add center rectangle origin'
|
|
||||||
data: [x: number, y: number]
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: 'Add circle origin'
|
type: 'Add circle origin'
|
||||||
data: [x: number, y: number]
|
data: [x: number, y: number]
|
||||||
@ -292,7 +278,6 @@ export type ModelingMachineEvent =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
| { type: 'Finish rectangle' }
|
| { type: 'Finish rectangle' }
|
||||||
| { type: 'Finish center rectangle' }
|
|
||||||
| { type: 'Finish circle' }
|
| { type: 'Finish circle' }
|
||||||
| { type: 'Artifact graph populated' }
|
| { type: 'Artifact graph populated' }
|
||||||
| { type: 'Artifact graph emptied' }
|
| { type: 'Artifact graph emptied' }
|
||||||
@ -521,9 +506,6 @@ export const modelingMachine = setup({
|
|||||||
'next is rectangle': ({ context: { sketchDetails, currentTool } }) =>
|
'next is rectangle': ({ context: { sketchDetails, currentTool } }) =>
|
||||||
currentTool === 'rectangle' &&
|
currentTool === 'rectangle' &&
|
||||||
canRectangleOrCircleTool({ sketchDetails }),
|
canRectangleOrCircleTool({ sketchDetails }),
|
||||||
'next is center rectangle': ({ context: { sketchDetails, currentTool } }) =>
|
|
||||||
currentTool === 'center rectangle' &&
|
|
||||||
canRectangleOrCircleTool({ sketchDetails }),
|
|
||||||
'next is circle': ({ context: { sketchDetails, currentTool } }) =>
|
'next is circle': ({ context: { sketchDetails, currentTool } }) =>
|
||||||
currentTool === 'circle' && canRectangleOrCircleTool({ sketchDetails }),
|
currentTool === 'circle' && canRectangleOrCircleTool({ sketchDetails }),
|
||||||
'next is line': ({ context }) => context.currentTool === 'line',
|
'next is line': ({ context }) => context.currentTool === 'line',
|
||||||
@ -824,26 +806,6 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
'listen for center rectangle origin': ({ context: { sketchDetails } }) => {
|
|
||||||
if (!sketchDetails) return
|
|
||||||
// setupNoPointsListener has the code for startProfileAt onClick
|
|
||||||
sceneEntitiesManager.setupNoPointsListener({
|
|
||||||
sketchDetails,
|
|
||||||
afterClick: (args) => {
|
|
||||||
const twoD = args.intersectionPoint?.twoD
|
|
||||||
if (twoD) {
|
|
||||||
sceneInfra.modelingSend({
|
|
||||||
type: 'Add center rectangle origin',
|
|
||||||
data: [twoD.x, twoD.y],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.error('No intersection point found')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
'listen for circle origin': ({ context: { sketchDetails } }) => {
|
'listen for circle origin': ({ context: { sketchDetails } }) => {
|
||||||
if (!sketchDetails) return
|
if (!sketchDetails) return
|
||||||
sceneEntitiesManager.createIntersectionPlane()
|
sceneEntitiesManager.createIntersectionPlane()
|
||||||
@ -897,21 +859,6 @@ export const modelingMachine = setup({
|
|||||||
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
'set up draft center rectangle': ({
|
|
||||||
context: { sketchDetails },
|
|
||||||
event,
|
|
||||||
}) => {
|
|
||||||
if (event.type !== 'Add center rectangle origin') return
|
|
||||||
if (!sketchDetails || !event.data) return
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
sceneEntitiesManager.setupDraftCenterRectangle(
|
|
||||||
sketchDetails.sketchPathToNode,
|
|
||||||
sketchDetails.zAxis,
|
|
||||||
sketchDetails.yAxis,
|
|
||||||
sketchDetails.origin,
|
|
||||||
event.data
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'set up draft circle': ({ context: { sketchDetails }, event }) => {
|
'set up draft circle': ({ context: { sketchDetails }, event }) => {
|
||||||
if (event.type !== 'Add circle origin') return
|
if (event.type !== 'Add circle origin') return
|
||||||
if (!sketchDetails || !event.data) return
|
if (!sketchDetails || !event.data) return
|
||||||
@ -1403,7 +1350,7 @@ export const modelingMachine = setup({
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
'animate-to-face': fromPromise(
|
'animate-to-face': fromPromise(
|
||||||
async (_: { input?: ExtrudeFacePlane | DefaultPlane | OffsetPlane }) => {
|
async (_: { input?: ExtrudeFacePlane | DefaultPlane }) => {
|
||||||
return {} as
|
return {} as
|
||||||
| undefined
|
| undefined
|
||||||
| {
|
| {
|
||||||
@ -1875,40 +1822,6 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
'Center Rectangle tool': {
|
|
||||||
entry: ['listen for center rectangle origin'],
|
|
||||||
|
|
||||||
states: {
|
|
||||||
'Awaiting corner': {
|
|
||||||
on: {
|
|
||||||
'Finish center rectangle': 'Finished Center Rectangle',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Awaiting origin': {
|
|
||||||
on: {
|
|
||||||
'Add center rectangle origin': {
|
|
||||||
target: 'Awaiting corner',
|
|
||||||
// TODO
|
|
||||||
actions: 'set up draft center rectangle',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'Finished Center Rectangle': {
|
|
||||||
always: '#Modeling.Sketch.SketchIdle',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
initial: 'Awaiting origin',
|
|
||||||
|
|
||||||
on: {
|
|
||||||
'change tool': {
|
|
||||||
target: 'Change Tool',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'clean slate': {
|
'clean slate': {
|
||||||
always: 'SketchIdle',
|
always: 'SketchIdle',
|
||||||
},
|
},
|
||||||
@ -2102,10 +2015,6 @@ export const modelingMachine = setup({
|
|||||||
target: 'Circle tool',
|
target: 'Circle tool',
|
||||||
guard: 'next is circle',
|
guard: 'next is circle',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
target: 'Center Rectangle tool',
|
|
||||||
guard: 'next is center rectangle',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
|
||||||
entry: 'assign tool in context',
|
entry: 'assign tool in context',
|
||||||
|
3
src/wasm-lib/Cargo.lock
generated
@ -1589,8 +1589,6 @@ dependencies = [
|
|||||||
"console",
|
"console",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
"pest",
|
|
||||||
"pest_derive",
|
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"similar",
|
"similar",
|
||||||
@ -1691,7 +1689,6 @@ dependencies = [
|
|||||||
"databake",
|
"databake",
|
||||||
"derive-docs",
|
"derive-docs",
|
||||||
"expectorate",
|
"expectorate",
|
||||||
"fnv",
|
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures",
|
"futures",
|
||||||
"git_rev",
|
"git_rev",
|
||||||
|
@ -9,16 +9,21 @@ new-test name:
|
|||||||
lint:
|
lint:
|
||||||
cargo clippy --workspace --all-targets -- -D warnings
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
|
||||||
# Generate the stdlib image artifacts
|
|
||||||
# Then run the stdlib docs generation
|
|
||||||
redo-kcl-stdlib-docs:
|
redo-kcl-stdlib-docs:
|
||||||
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib kcl_test_example
|
|
||||||
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
|
EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
|
||||||
|
|
||||||
# Create a new KCL deterministic simulation test case.
|
# Create a new KCL deterministic simulation test case.
|
||||||
new-sim-test test_name render_to_png="true":
|
new-sim-test test_name kcl_program render_to_png="true":
|
||||||
|
# Each test file gets its own directory. This will contain the KCL program, and its
|
||||||
|
# snapshotted artifacts (e.g. serialized tokens, serialized ASTs, program memory,
|
||||||
|
# PNG snapshots, etc).
|
||||||
|
mkdir kcl/tests/{{test_name}}
|
||||||
|
echo "{{kcl_program}}" > kcl/tests/{{test_name}}/input.kcl
|
||||||
|
# Add the various tests for this new test case.
|
||||||
|
cat kcl/tests/simtest.tmpl | sed "s/TEST_NAME_HERE/{{test_name}}/" | sed "s/RENDER_TO_PNG/{{render_to_png}}/" >> kcl/src/simulation_tests.rs
|
||||||
|
# Run all the tests for the first time, in the right order.
|
||||||
{{cita}} -p kcl-lib -- tests::{{test_name}}::tokenize
|
{{cita}} -p kcl-lib -- tests::{{test_name}}::tokenize
|
||||||
{{cita}} -p kcl-lib -- tests::{{test_name}}::parse
|
{{cita}} -p kcl-lib -- tests::{{test_name}}::parse
|
||||||
{{cita}} -p kcl-lib -- tests::{{test_name}}::unparse
|
{{cita}} -p kcl-lib -- tests::{{test_name}}::unparse
|
||||||
TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib -- tests::{{test_name}}::kcl_test_execute
|
TWENTY_TWENTY=overwrite {{cita}} -p kcl-lib -- tests::{{test_name}}::kcl_test_execute
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ convert_case = "0.6.0"
|
|||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
databake = { version = "0.1.8", features = ["derive"] }
|
databake = { version = "0.1.8", features = ["derive"] }
|
||||||
derive-docs = { version = "0.1.29", path = "../derive-docs" }
|
derive-docs = { version = "0.1.29", path = "../derive-docs" }
|
||||||
fnv = "1.0.7"
|
|
||||||
form_urlencoded = "1.2.1"
|
form_urlencoded = "1.2.1"
|
||||||
futures = { version = "0.3.31" }
|
futures = { version = "0.3.31" }
|
||||||
git_rev = "0.1.0"
|
git_rev = "0.1.0"
|
||||||
@ -87,7 +86,7 @@ expectorate = "1.1.0"
|
|||||||
handlebars = "6.2.0"
|
handlebars = "6.2.0"
|
||||||
iai = "0.1"
|
iai = "0.1"
|
||||||
image = { version = "0.25.5", default-features = false, features = ["png"] }
|
image = { version = "0.25.5", default-features = false, features = ["png"] }
|
||||||
insta = { version = "1.41.1", features = ["json", "filters", "redactions"] }
|
insta = { version = "1.41.1", features = ["json", "filters"] }
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
|
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||||
|
@ -39,5 +39,5 @@ const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_sv
|
|||||||
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
||||||
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
||||||
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
|
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
|
||||||
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../tests/mike_stress_test/input.kcl");
|
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");
|
||||||
const LSYSTEM_KOCH_SNOWFLAKE_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");
|
const LSYSTEM_KOCH_SNOWFLAKE_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");
|
||||||
|
@ -28,5 +28,5 @@ const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_sv
|
|||||||
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
||||||
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
||||||
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
|
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
|
||||||
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../tests/mike_stress_test/input.kcl");
|
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");
|
||||||
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");
|
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");
|
||||||
|
@ -62,6 +62,6 @@ const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_sv
|
|||||||
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
|
||||||
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
|
||||||
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
|
const MATH_PROGRAM: &str = include_str!("../../tests/executor/inputs/math.kcl");
|
||||||
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../tests/mike_stress_test/input.kcl");
|
const MIKE_STRESS_TEST_PROGRAM: &str = include_str!("../../tests/executor/inputs/mike_stress_test.kcl");
|
||||||
const GLOBAL_TAGS_FILE: &str = include_str!("../../tests/executor/inputs/global-tags.kcl");
|
const GLOBAL_TAGS_FILE: &str = include_str!("../../tests/executor/inputs/global-tags.kcl");
|
||||||
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");
|
const LSYSTEM_PROGRAM: &str = include_str!("../../tests/executor/inputs/lsystem.kcl");
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
// This file is used by the import docs.
|
|
||||||
|
|
||||||
export fn width = () => {
|
|
||||||
return 10
|
|
||||||
}
|
|
||||||
|
|
||||||
export fn height = () => {
|
|
||||||
return 10
|
|
||||||
}
|
|
||||||
|
|
||||||
export fn buildSketch = (plane, offset) => {
|
|
||||||
w = width()
|
|
||||||
h = height()
|
|
||||||
return startSketchOn(plane)
|
|
||||||
|> startProfileAt(offset, %)
|
|
||||||
|> line([w, 0], %)
|
|
||||||
|> line([0, h], %)
|
|
||||||
|> line([-w, 0], %)
|
|
||||||
|> close(%)
|
|
||||||
}
|
|
@ -7,7 +7,6 @@ layout: manual
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
* [Types](kcl/types)
|
* [Types](kcl/types)
|
||||||
* [Modules](kcl/modules)
|
|
||||||
* [Known Issues](kcl/KNOWN-ISSUES)
|
* [Known Issues](kcl/KNOWN-ISSUES)
|
||||||
{{#each functions}}
|
{{#each functions}}
|
||||||
* [`{{name}}`](kcl/{{name}})
|
* [`{{name}}`](kcl/{{name}})
|
||||||
|
@ -801,17 +801,6 @@ impl Plane {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The standard planes are XY, YZ and XZ (in both positive and negative)
|
|
||||||
pub fn is_standard(&self) -> bool {
|
|
||||||
!self.is_custom()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The standard planes are XY, YZ and XZ (in both positive and negative)
|
|
||||||
/// Custom planes are any other plane that the user might specify.
|
|
||||||
pub fn is_custom(&self) -> bool {
|
|
||||||
matches!(self.value, PlaneType::Custom)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
@ -1060,14 +1049,6 @@ impl KclValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_plane(&self) -> Option<&Plane> {
|
|
||||||
if let KclValue::Plane(value) = &self {
|
|
||||||
Some(value)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_solid(&self) -> Option<&Solid> {
|
pub fn as_solid(&self) -> Option<&Solid> {
|
||||||
if let KclValue::Solid(value) = &self {
|
if let KclValue::Solid(value) = &self {
|
||||||
Some(value)
|
Some(value)
|
||||||
@ -3052,14 +3033,14 @@ for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_get_member_of_array_with_function() {
|
async fn test_get_member_of_array_with_function() {
|
||||||
let ast = r#"fn box = (arr) => {
|
let ast = r#"fn box = (array) => {
|
||||||
let myBox =startSketchOn('XY')
|
let myBox =startSketchOn('XY')
|
||||||
|> startProfileAt(arr[0], %)
|
|> startProfileAt(array[0], %)
|
||||||
|> line([0, arr[1]], %)
|
|> line([0, array[1]], %)
|
||||||
|> line([arr[2], 0], %)
|
|> line([array[2], 0], %)
|
||||||
|> line([0, -arr[1]], %)
|
|> line([0, -array[1]], %)
|
||||||
|> close(%)
|
|> close(%)
|
||||||
|> extrude(arr[3], %)
|
|> extrude(array[3], %)
|
||||||
|
|
||||||
return myBox
|
return myBox
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use tower_lsp::{
|
use tower_lsp::{
|
||||||
lsp_types::{Diagnostic, SemanticTokenModifier, SemanticTokenType},
|
lsp_types::{SemanticTokenModifier, SemanticTokenType},
|
||||||
LanguageServer,
|
LanguageServer,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2369,14 +2369,7 @@ async fn kcl_test_kcl_lsp_diagnostics_on_execution_error() {
|
|||||||
|
|
||||||
// Get the diagnostics.
|
// Get the diagnostics.
|
||||||
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
|
let diagnostics = server.diagnostics_map.get("file:///test.kcl");
|
||||||
if let Some(diagnostics) = diagnostics {
|
assert!(diagnostics.is_none());
|
||||||
let ds: Vec<Diagnostic> = diagnostics.to_owned();
|
|
||||||
eprintln!("Expected no diagnostics, but found some.");
|
|
||||||
for d in ds {
|
|
||||||
eprintln!("{:?}: {}", d.severity, d.message);
|
|
||||||
}
|
|
||||||
panic!();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
@ -2040,39 +2040,11 @@ fn fn_call(i: TokenSlice) -> PResult<Node<CallExpression>> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use itertools::Itertools;
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::ast::types::{BodyItem, Expr, ModuleId, VariableKind};
|
use crate::ast::types::{BodyItem, Expr, ModuleId, VariableKind};
|
||||||
|
|
||||||
fn assert_reserved(word: &str) {
|
|
||||||
// Try to use it as a variable name.
|
|
||||||
let code = format!(r#"{} = 0"#, word);
|
|
||||||
let result = crate::parser::top_level_parse(code.as_str());
|
|
||||||
let err = result.unwrap_err();
|
|
||||||
// Which token causes the error may change. In "return = 0", for
|
|
||||||
// example, "return" is the problem.
|
|
||||||
assert!(
|
|
||||||
err.message().starts_with("Unexpected token: ")
|
|
||||||
|| err
|
|
||||||
.message()
|
|
||||||
.starts_with("Cannot assign a variable to a reserved keyword: "),
|
|
||||||
"Error message is: {}",
|
|
||||||
err.message(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reserved_words() {
|
|
||||||
// Since these are stored in a set, we sort to make the tests
|
|
||||||
// deterministic.
|
|
||||||
for word in crate::token::RESERVED_WORDS.keys().sorted() {
|
|
||||||
assert_reserved(word);
|
|
||||||
}
|
|
||||||
assert_reserved("import");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_args() {
|
fn parse_args() {
|
||||||
for (i, (test, expected_len)) in [("someVar", 1), ("5, 3", 2), (r#""a""#, 1)].into_iter().enumerate() {
|
for (i, (test, expected_len)) in [("someVar", 1), ("5, 3", 2), (r#""a""#, 1)].into_iter().enumerate() {
|
||||||
|
@ -5,7 +5,7 @@ pub mod project;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use parse_display::{Display, FromStr};
|
use parse_display::{Display, FromStr};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserializer, Deserialize, Serialize};
|
||||||
use validator::{Validate, ValidateRange};
|
use validator::{Validate, ValidateRange};
|
||||||
|
|
||||||
const DEFAULT_THEME_COLOR: f64 = 264.5;
|
const DEFAULT_THEME_COLOR: f64 = 264.5;
|
||||||
@ -119,12 +119,34 @@ pub struct AppSettings {
|
|||||||
/// This setting only applies to the web app. And is temporary until we have Linux support.
|
/// This setting only applies to the web app. And is temporary until we have Linux support.
|
||||||
#[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
|
#[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
|
||||||
pub dismiss_web_banner: bool,
|
pub dismiss_web_banner: bool,
|
||||||
/// When the user is idle, and this is true, the stream will be torn down.
|
/// When the user is idle, teardown the stream after some time.
|
||||||
#[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")]
|
#[serde(default, deserialize_with = "deserialize_stream_idle_mode", alias = "streamIdleMode", skip_serializing_if = "is_default")]
|
||||||
stream_idle_mode: bool,
|
stream_idle_mode: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_stream_idle_mode<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum StreamIdleModeValue {
|
||||||
|
String(String),
|
||||||
|
Boolean(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT: u32 = 1000 * 60 * 5;
|
||||||
|
|
||||||
|
Ok(match StreamIdleModeValue::deserialize(deserializer) {
|
||||||
|
Ok(StreamIdleModeValue::String(value)) => Some(value.parse::<u32>().unwrap_or(DEFAULT_TIMEOUT)),
|
||||||
|
// The old type of this value. I'm willing to say no one used it but
|
||||||
|
// we can never guarantee it.
|
||||||
|
Ok(StreamIdleModeValue::Boolean(true)) => Some(DEFAULT_TIMEOUT),
|
||||||
|
Ok(StreamIdleModeValue::Boolean(false)) => None,
|
||||||
|
_ => None
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: When we remove backwards compatibility with the old settings file, we can remove this.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
@ -582,7 +604,7 @@ textWrapping = true
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::In,
|
base_unit: UnitLength::In,
|
||||||
@ -643,7 +665,7 @@ includeSettings = false
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
@ -709,7 +731,7 @@ defaultProjectName = "projects-$nnn"
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
@ -787,7 +809,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Mm,
|
base_unit: UnitLength::Mm,
|
||||||
|
@ -123,7 +123,7 @@ includeSettings = false
|
|||||||
theme_color: None,
|
theme_color: None,
|
||||||
dismiss_web_banner: false,
|
dismiss_web_banner: false,
|
||||||
enable_ssao: None,
|
enable_ssao: None,
|
||||||
stream_idle_mode: false,
|
stream_idle_mode: None,
|
||||||
},
|
},
|
||||||
modeling: ModelingSettings {
|
modeling: ModelingSettings {
|
||||||
base_unit: UnitLength::Yd,
|
base_unit: UnitLength::Yd,
|
||||||
|
@ -102,79 +102,26 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
|||||||
/// Take a starting value. Then, for each element of an array, calculate the next value,
|
/// Take a starting value. Then, for each element of an array, calculate the next value,
|
||||||
/// using the previous value and the element.
|
/// using the previous value and the element.
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// // This function adds two numbers.
|
/// fn decagon = (radius) => {
|
||||||
/// fn add = (a, b) => { return a + b }
|
/// let step = (1/10) * tau()
|
||||||
///
|
/// let sketch001 = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
|
||||||
/// // This function adds an array of numbers.
|
/// return reduce([1..10], sketch001, (i, sg) => {
|
||||||
/// // It uses the `reduce` function, to call the `add` function on every
|
/// let x = cos(step * i) * radius
|
||||||
/// // element of the `arr` parameter. The starting value is 0.
|
/// let y = sin(step * i) * radius
|
||||||
/// fn sum = (arr) => { return reduce(arr, 0, add) }
|
/// return lineTo([x, y], sg)
|
||||||
///
|
/// })
|
||||||
/// /*
|
/// }
|
||||||
/// The above is basically like this pseudo-code:
|
/// decagon(5.0) |> close(%)
|
||||||
/// fn sum(arr):
|
|
||||||
/// let sumSoFar = 0
|
|
||||||
/// for i in arr:
|
|
||||||
/// sumSoFar = add(sumSoFar, i)
|
|
||||||
/// return sumSoFar
|
|
||||||
/// */
|
|
||||||
///
|
|
||||||
/// // We use `assertEqual` to check that our `sum` function gives the
|
|
||||||
/// // expected result. It's good to check your work!
|
|
||||||
/// assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
|
|
||||||
/// ```
|
/// ```
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// // This example works just like the previous example above, but it uses
|
/// array = [1, 2, 3]
|
||||||
/// // an anonymous `add` function as its parameter, instead of declaring a
|
/// sum = reduce(array, 0, (i, result_so_far) => { return i + result_so_far })
|
||||||
/// // named function outside.
|
|
||||||
/// arr = [1, 2, 3]
|
|
||||||
/// sum = reduce(arr, 0, (i, result_so_far) => { return i + result_so_far })
|
|
||||||
///
|
|
||||||
/// // We use `assertEqual` to check that our `sum` function gives the
|
|
||||||
/// // expected result. It's good to check your work!
|
|
||||||
/// assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
|
/// assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
|
||||||
/// ```
|
/// ```
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// // Declare a function that sketches a decagon.
|
/// fn add = (a, b) => { return a + b }
|
||||||
/// fn decagon = (radius) => {
|
/// fn sum = (array) => { return reduce(array, 0, add) }
|
||||||
/// // Each side of the decagon is turned this many degrees from the previous angle.
|
/// assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
|
||||||
/// stepAngle = (1/10) * tau()
|
|
||||||
///
|
|
||||||
/// // Start the decagon sketch at this point.
|
|
||||||
/// startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
|
|
||||||
///
|
|
||||||
/// // Use a `reduce` to draw the remaining decagon sides.
|
|
||||||
/// // For each number in the array 1..10, run the given function,
|
|
||||||
/// // which takes a partially-sketched decagon and adds one more edge to it.
|
|
||||||
/// fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) => {
|
|
||||||
/// // Draw one edge of the decagon.
|
|
||||||
/// let x = cos(stepAngle * i) * radius
|
|
||||||
/// let y = sin(stepAngle * i) * radius
|
|
||||||
/// return lineTo([x, y], partialDecagon)
|
|
||||||
/// })
|
|
||||||
///
|
|
||||||
/// return fullDecagon
|
|
||||||
///
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// /*
|
|
||||||
/// The `decagon` above is basically like this pseudo-code:
|
|
||||||
/// fn decagon(radius):
|
|
||||||
/// let stepAngle = (1/10) * tau()
|
|
||||||
/// let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
|
|
||||||
///
|
|
||||||
/// // Here's the reduce part.
|
|
||||||
/// let partialDecagon = startOfDecagonSketch
|
|
||||||
/// for i in [1..10]:
|
|
||||||
/// let x = cos(stepAngle * i) * radius
|
|
||||||
/// let y = sin(stepAngle * i) * radius
|
|
||||||
/// partialDecagon = lineTo([x, y], partialDecagon)
|
|
||||||
/// fullDecagon = partialDecagon // it's now full
|
|
||||||
/// return fullDecagon
|
|
||||||
/// */
|
|
||||||
///
|
|
||||||
/// // Use the `decagon` function declared above, to sketch a decagon with radius 5.
|
|
||||||
/// decagon(5.0) |> close(%)
|
|
||||||
/// ```
|
/// ```
|
||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "reduce",
|
name = "reduce",
|
||||||
|
@ -144,9 +144,6 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
|||||||
/// Note: The import command currently only works when using the native
|
/// Note: The import command currently only works when using the native
|
||||||
/// Modeling App.
|
/// Modeling App.
|
||||||
///
|
///
|
||||||
/// For importing KCL functions using the `import` statement, see the docs on
|
|
||||||
/// [KCL modules](/docs/kcl/modules).
|
|
||||||
///
|
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// const model = import("tests/inputs/cube.obj")
|
/// const model = import("tests/inputs/cube.obj")
|
||||||
/// ```
|
/// ```
|
||||||
@ -166,15 +163,6 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
|||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// const model = import("tests/inputs/cube.step")
|
/// const model = import("tests/inputs/cube.step")
|
||||||
/// ```
|
/// ```
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// import height, buildSketch from 'common.kcl'
|
|
||||||
///
|
|
||||||
/// plane = 'XZ'
|
|
||||||
/// margin = 2
|
|
||||||
/// s1 = buildSketch(plane, [0, 0])
|
|
||||||
/// s2 = buildSketch(plane, [0, height() + margin])
|
|
||||||
/// ```
|
|
||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "import",
|
name = "import",
|
||||||
tags = [],
|
tags = [],
|
||||||
|
@ -91,7 +91,6 @@ lazy_static! {
|
|||||||
Box::new(crate::std::sketch::ProfileStart),
|
Box::new(crate::std::sketch::ProfileStart),
|
||||||
Box::new(crate::std::sketch::Close),
|
Box::new(crate::std::sketch::Close),
|
||||||
Box::new(crate::std::sketch::Arc),
|
Box::new(crate::std::sketch::Arc),
|
||||||
Box::new(crate::std::sketch::ArcTo),
|
|
||||||
Box::new(crate::std::sketch::TangentialArc),
|
Box::new(crate::std::sketch::TangentialArc),
|
||||||
Box::new(crate::std::sketch::TangentialArcTo),
|
Box::new(crate::std::sketch::TangentialArcTo),
|
||||||
Box::new(crate::std::sketch::TangentialArcToRelative),
|
Box::new(crate::std::sketch::TangentialArcToRelative),
|
||||||
|
@ -462,7 +462,7 @@ fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<P
|
|||||||
// Gets an f64 from a KCL value.
|
// Gets an f64 from a KCL value.
|
||||||
let f = |k: &KclValue, component: char| {
|
let f = |k: &KclValue, component: char| {
|
||||||
use super::args::FromKclValue;
|
use super::args::FromKclValue;
|
||||||
if let Some(value) = f64::from_kcl_val(k) {
|
if let Some(value) = f64::from_mem_item(k) {
|
||||||
Ok(value)
|
Ok(value)
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
//! Standard library plane helpers.
|
//! Standard library plane helpers.
|
||||||
|
|
||||||
use derive_docs::stdlib;
|
use derive_docs::stdlib;
|
||||||
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Color, ModelingCmd};
|
|
||||||
use kittycad_modeling_cmds as kcmc;
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::KclError,
|
errors::KclError,
|
||||||
executor::{ExecState, KclValue, Plane, PlaneType},
|
executor::{ExecState, KclValue, Plane},
|
||||||
std::{sketch::PlaneData, Args},
|
std::{sketch::PlaneData, Args},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// One of the standard planes.
|
/// One of the standard planes.
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, JsonSchema)]
|
||||||
#[ts(export)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum StandardPlane {
|
pub enum StandardPlane {
|
||||||
/// The XY plane.
|
/// The XY plane.
|
||||||
@ -53,8 +50,8 @@ impl From<StandardPlane> for PlaneData {
|
|||||||
/// Offset a plane by a distance along its normal.
|
/// Offset a plane by a distance along its normal.
|
||||||
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
|
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?;
|
||||||
let plane = inner_offset_plane(std_plane, offset, exec_state).await?;
|
let plane_data = inner_offset_plane(std_plane, offset, exec_state).await?;
|
||||||
make_offset_plane_in_engine(&plane, exec_state, &args).await?;
|
let plane = Plane::from_plane_data(plane_data, exec_state);
|
||||||
Ok(KclValue::Plane(Box::new(plane)))
|
Ok(KclValue::Plane(Box::new(plane)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,14 +144,11 @@ async fn inner_offset_plane(
|
|||||||
std_plane: StandardPlane,
|
std_plane: StandardPlane,
|
||||||
offset: f64,
|
offset: f64,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
) -> Result<Plane, KclError> {
|
) -> Result<PlaneData, KclError> {
|
||||||
// Convert to the plane type.
|
// Convert to the plane type.
|
||||||
let plane_data: PlaneData = std_plane.into();
|
let plane_data: PlaneData = std_plane.into();
|
||||||
// Convert to a plane.
|
// Convert to a plane.
|
||||||
let mut plane = Plane::from_plane_data(plane_data, exec_state);
|
let mut plane = Plane::from_plane_data(plane_data, exec_state);
|
||||||
// Though offset planes are derived from standard planes, they are not
|
|
||||||
// standard planes themselves.
|
|
||||||
plane.value = PlaneType::Custom;
|
|
||||||
|
|
||||||
match std_plane {
|
match std_plane {
|
||||||
StandardPlane::XY => {
|
StandardPlane::XY => {
|
||||||
@ -177,44 +171,10 @@ async fn inner_offset_plane(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(plane)
|
Ok(PlaneData::Plane {
|
||||||
}
|
origin: Box::new(plane.origin),
|
||||||
|
x_axis: Box::new(plane.x_axis),
|
||||||
// Engine-side effectful creation of an actual plane object.
|
y_axis: Box::new(plane.y_axis),
|
||||||
// offset planes are shown by default, and hidden by default if they
|
z_axis: Box::new(plane.z_axis),
|
||||||
// are used as a sketch plane. That hiding command is sent within inner_start_profile_at
|
})
|
||||||
async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> {
|
|
||||||
// Create new default planes.
|
|
||||||
let default_size = 100.0;
|
|
||||||
let color = Color {
|
|
||||||
r: 0.6,
|
|
||||||
g: 0.6,
|
|
||||||
b: 0.6,
|
|
||||||
a: 0.3,
|
|
||||||
};
|
|
||||||
|
|
||||||
args.batch_modeling_cmd(
|
|
||||||
plane.id,
|
|
||||||
ModelingCmd::from(mcmd::MakePlane {
|
|
||||||
clobber: false,
|
|
||||||
origin: plane.origin.into(),
|
|
||||||
size: LengthUnit(default_size),
|
|
||||||
x_axis: plane.x_axis.into(),
|
|
||||||
y_axis: plane.y_axis.into(),
|
|
||||||
hide: Some(false),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Set the color.
|
|
||||||
args.batch_modeling_cmd(
|
|
||||||
exec_state.id_generator.next_uuid(),
|
|
||||||
ModelingCmd::from(mcmd::PlaneSetColor {
|
|
||||||
color,
|
|
||||||
plane_id: plane.id,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
@ -894,7 +894,7 @@ pub async fn start_sketch_at(exec_state: &mut ExecState, args: Args) -> Result<K
|
|||||||
async fn inner_start_sketch_at(data: [f64; 2], exec_state: &mut ExecState, args: Args) -> Result<Sketch, KclError> {
|
async fn inner_start_sketch_at(data: [f64; 2], exec_state: &mut ExecState, args: Args) -> Result<Sketch, KclError> {
|
||||||
// Let's assume it's the XY plane for now, this is just for backwards compatibility.
|
// Let's assume it's the XY plane for now, this is just for backwards compatibility.
|
||||||
let xy_plane = PlaneData::XY;
|
let xy_plane = PlaneData::XY;
|
||||||
let sketch_surface = inner_start_sketch_on(SketchData::PlaneOrientation(xy_plane), None, exec_state, &args).await?;
|
let sketch_surface = inner_start_sketch_on(SketchData::Plane(xy_plane), None, exec_state, &args).await?;
|
||||||
let sketch = inner_start_profile_at(data, sketch_surface, None, exec_state, args).await?;
|
let sketch = inner_start_profile_at(data, sketch_surface, None, exec_state, args).await?;
|
||||||
Ok(sketch)
|
Ok(sketch)
|
||||||
}
|
}
|
||||||
@ -905,12 +905,11 @@ async fn inner_start_sketch_at(data: [f64; 2], exec_state: &mut ExecState, args:
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase", untagged)]
|
#[serde(rename_all = "camelCase", untagged)]
|
||||||
pub enum SketchData {
|
pub enum SketchData {
|
||||||
PlaneOrientation(PlaneData),
|
Plane(PlaneData),
|
||||||
Plane(Box<Plane>),
|
|
||||||
Solid(Box<Solid>),
|
Solid(Box<Solid>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Orientation data that can be used to construct a plane, not a plane in itself.
|
/// Data for a plane.
|
||||||
#[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(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -1070,11 +1069,10 @@ async fn inner_start_sketch_on(
|
|||||||
args: &Args,
|
args: &Args,
|
||||||
) -> Result<SketchSurface, KclError> {
|
) -> Result<SketchSurface, KclError> {
|
||||||
match data {
|
match data {
|
||||||
SketchData::PlaneOrientation(plane_data) => {
|
SketchData::Plane(plane_data) => {
|
||||||
let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
|
let plane = start_sketch_on_plane(plane_data, exec_state, args).await?;
|
||||||
Ok(SketchSurface::Plane(plane))
|
Ok(SketchSurface::Plane(plane))
|
||||||
}
|
}
|
||||||
SketchData::Plane(plane) => Ok(SketchSurface::Plane(plane)),
|
|
||||||
SketchData::Solid(solid) => {
|
SketchData::Solid(solid) => {
|
||||||
let Some(tag) = tag else {
|
let Some(tag) = tag else {
|
||||||
return Err(KclError::Type(KclErrorDetails {
|
return Err(KclError::Type(KclErrorDetails {
|
||||||
@ -1108,7 +1106,7 @@ async fn start_sketch_on_face(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn make_sketch_plane_from_orientation(
|
async fn start_sketch_on_plane(
|
||||||
data: PlaneData,
|
data: PlaneData,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
@ -1124,10 +1122,10 @@ async fn make_sketch_plane_from_orientation(
|
|||||||
|
|
||||||
plane.id = match data {
|
plane.id = match data {
|
||||||
PlaneData::XY => default_planes.xy,
|
PlaneData::XY => default_planes.xy,
|
||||||
PlaneData::NegXY => default_planes.neg_xy,
|
|
||||||
PlaneData::XZ => default_planes.xz,
|
PlaneData::XZ => default_planes.xz,
|
||||||
PlaneData::NegXZ => default_planes.neg_xz,
|
|
||||||
PlaneData::YZ => default_planes.yz,
|
PlaneData::YZ => default_planes.yz,
|
||||||
|
PlaneData::NegXY => default_planes.neg_xy,
|
||||||
|
PlaneData::NegXZ => default_planes.neg_xz,
|
||||||
PlaneData::NegYZ => default_planes.neg_yz,
|
PlaneData::NegYZ => default_planes.neg_yz,
|
||||||
PlaneData::Plane {
|
PlaneData::Plane {
|
||||||
origin,
|
origin,
|
||||||
@ -1212,26 +1210,11 @@ pub(crate) async fn inner_start_profile_at(
|
|||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
args: Args,
|
args: Args,
|
||||||
) -> Result<Sketch, KclError> {
|
) -> Result<Sketch, KclError> {
|
||||||
match &sketch_surface {
|
if let SketchSurface::Face(face) = &sketch_surface {
|
||||||
SketchSurface::Face(face) => {
|
// Flush the batch for our fillets/chamfers if there are any.
|
||||||
// Flush the batch for our fillets/chamfers if there are any.
|
// If we do not do these for sketch on face, things will fail with face does not exist.
|
||||||
// If we do not do these for sketch on face, things will fail with face does not exist.
|
args.flush_batch_for_solid_set(exec_state, face.solid.clone().into())
|
||||||
args.flush_batch_for_solid_set(exec_state, face.solid.clone().into())
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
SketchSurface::Plane(plane) if !plane.is_standard() => {
|
|
||||||
// Hide whatever plane we are sketching on.
|
|
||||||
// This is especially helpful for offset planes, which would be visible otherwise.
|
|
||||||
args.batch_end_cmd(
|
|
||||||
exec_state.id_generator.next_uuid(),
|
|
||||||
ModelingCmd::from(mcmd::ObjectVisible {
|
|
||||||
object_id: plane.id,
|
|
||||||
hidden: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter sketch mode on the surface.
|
// Enter sketch mode on the surface.
|
||||||
@ -1486,17 +1469,6 @@ pub enum ArcData {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data to draw a three point arc (arcTo).
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
|
||||||
#[ts(export)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ArcToData {
|
|
||||||
/// End point of the arc. A point in 3D space
|
|
||||||
pub end: [f64; 2],
|
|
||||||
/// Interior point of the arc. A point in 3D space
|
|
||||||
pub interior: [f64; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw an arc.
|
/// Draw an arc.
|
||||||
pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||||
let (data, sketch, tag): (ArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
|
let (data, sketch, tag): (ArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
|
||||||
@ -1527,7 +1499,7 @@ pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
|||||||
/// radius: 16
|
/// radius: 16
|
||||||
/// }, %)
|
/// }, %)
|
||||||
/// |> close(%)
|
/// |> close(%)
|
||||||
/// const example = extrude(10, exampleSketch)
|
// const example = extrude(10, exampleSketch)
|
||||||
/// ```
|
/// ```
|
||||||
#[stdlib {
|
#[stdlib {
|
||||||
name = "arc",
|
name = "arc",
|
||||||
@ -1606,104 +1578,6 @@ pub(crate) async fn inner_arc(
|
|||||||
Ok(new_sketch)
|
Ok(new_sketch)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a three point arc.
|
|
||||||
pub async fn arc_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
|
||||||
let (data, sketch, tag): (ArcToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
|
|
||||||
|
|
||||||
let new_sketch = inner_arc_to(data, sketch, tag, exec_state, args).await?;
|
|
||||||
Ok(KclValue::Sketch {
|
|
||||||
value: Box::new(new_sketch),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw a 3 point arc.
|
|
||||||
///
|
|
||||||
/// The arc is constructed such that the start point is the current position of the sketch and two more points defined as the end and interior point.
|
|
||||||
/// The interior point is placed between the start point and end point. The radius of the arc will be controlled by how far the interior point is placed from
|
|
||||||
/// the start and end.
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// const exampleSketch = startSketchOn('XZ')
|
|
||||||
/// |> startProfileAt([0, 0], %)
|
|
||||||
/// |> arcTo({
|
|
||||||
/// end: [10,0],
|
|
||||||
/// interior: [5,5]
|
|
||||||
/// }, %)
|
|
||||||
/// |> close(%)
|
|
||||||
/// const example = extrude(10, exampleSketch)
|
|
||||||
/// ```
|
|
||||||
#[stdlib {
|
|
||||||
name = "arcTo",
|
|
||||||
}]
|
|
||||||
pub(crate) async fn inner_arc_to(
|
|
||||||
data: ArcToData,
|
|
||||||
sketch: Sketch,
|
|
||||||
tag: Option<TagNode>,
|
|
||||||
exec_state: &mut ExecState,
|
|
||||||
args: Args,
|
|
||||||
) -> Result<Sketch, KclError> {
|
|
||||||
let from: Point2d = sketch.current_pen_position()?;
|
|
||||||
let id = exec_state.id_generator.next_uuid();
|
|
||||||
|
|
||||||
// The start point is taken from the path you are extending.
|
|
||||||
args.batch_modeling_cmd(
|
|
||||||
id,
|
|
||||||
ModelingCmd::from(mcmd::ExtendPath {
|
|
||||||
path: sketch.id.into(),
|
|
||||||
segment: PathSegment::ArcTo {
|
|
||||||
end: kcmc::shared::Point3d {
|
|
||||||
x: LengthUnit(data.end[0]),
|
|
||||||
y: LengthUnit(data.end[1]),
|
|
||||||
z: LengthUnit(0.0),
|
|
||||||
},
|
|
||||||
interior: kcmc::shared::Point3d {
|
|
||||||
x: LengthUnit(data.interior[0]),
|
|
||||||
y: LengthUnit(data.interior[1]),
|
|
||||||
z: LengthUnit(0.0),
|
|
||||||
},
|
|
||||||
relative: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let start = [from.x, from.y];
|
|
||||||
let interior = [data.interior[0], data.interior[1]];
|
|
||||||
let end = [data.end[0], data.end[1]];
|
|
||||||
|
|
||||||
// compute the center of the circle since we do not have the value returned from the engine
|
|
||||||
let center = calculate_circle_center(start, interior, end);
|
|
||||||
|
|
||||||
// compute the radius since we do not have the value returned from the engine
|
|
||||||
// Pick any of the 3 points since they all lie along the circle
|
|
||||||
let sum_of_square_differences =
|
|
||||||
(center[0] - start[0] * center[0] - start[0]) + (center[1] - start[1] * center[1] - start[1]);
|
|
||||||
let radius = sum_of_square_differences.sqrt();
|
|
||||||
|
|
||||||
let current_path = Path::Arc {
|
|
||||||
base: BasePath {
|
|
||||||
from: from.into(),
|
|
||||||
to: data.end,
|
|
||||||
tag: tag.clone(),
|
|
||||||
geo_meta: GeoMeta {
|
|
||||||
id,
|
|
||||||
metadata: args.source_range.into(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
center,
|
|
||||||
radius,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut new_sketch = sketch.clone();
|
|
||||||
if let Some(tag) = &tag {
|
|
||||||
new_sketch.add_tag(tag, ¤t_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
new_sketch.paths.push(current_path);
|
|
||||||
|
|
||||||
Ok(new_sketch)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Data to draw a tangential arc.
|
/// Data to draw a tangential arc.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@ -2023,42 +1897,6 @@ async fn inner_tangential_arc_to_relative(
|
|||||||
Ok(new_sketch)
|
Ok(new_sketch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the center of 3 points
|
|
||||||
// To calculate the center of the 3 point circle 2 perpendicular lines are created
|
|
||||||
// These perpendicular lines will intersect at the center of the circle.
|
|
||||||
fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64; 2] {
|
|
||||||
// y2 - y1
|
|
||||||
let y_2_1 = p2[1] - p1[1];
|
|
||||||
// y3 - y2
|
|
||||||
let y_3_2 = p3[1] - p2[1];
|
|
||||||
// x2 - x1
|
|
||||||
let x_2_1 = p2[0] - p1[0];
|
|
||||||
// x3 - x2
|
|
||||||
let x_3_2 = p3[0] - p2[0];
|
|
||||||
|
|
||||||
// Slope of two perpendicular lines
|
|
||||||
let slope_a = y_2_1 / x_2_1;
|
|
||||||
let slope_b = y_3_2 / x_3_2;
|
|
||||||
|
|
||||||
// Values for line intersection
|
|
||||||
// y1 - y3
|
|
||||||
let y_1_3 = p1[1] - p3[1];
|
|
||||||
// x1 + x2
|
|
||||||
let x_1_2 = p1[0] + p2[0];
|
|
||||||
// x2 + x3
|
|
||||||
let x_2_3 = p2[0] + p3[0];
|
|
||||||
// y1 + y2
|
|
||||||
let y_1_2 = p1[1] + p2[1];
|
|
||||||
|
|
||||||
// Solve for the intersection of these two lines
|
|
||||||
let numerator = (slope_a * slope_b * y_1_3) + (slope_b * x_1_2) - (slope_a * x_2_3);
|
|
||||||
let x = numerator / (2.0 * (slope_b - slope_a));
|
|
||||||
|
|
||||||
let y = ((-1.0 / slope_a) * (x - (x_1_2 / 2.0))) + (y_1_2 / 2.0);
|
|
||||||
|
|
||||||
[x, y]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Data to draw a bezier curve.
|
/// Data to draw a bezier curve.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@ -2235,7 +2073,7 @@ mod tests {
|
|||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use crate::{executor::TagIdentifier, std::sketch::calculate_circle_center, std::sketch::PlaneData};
|
use crate::{executor::TagIdentifier, std::sketch::PlaneData};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deserialize_plane_data() {
|
fn test_deserialize_plane_data() {
|
||||||
@ -2306,11 +2144,4 @@ mod tests {
|
|||||||
crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
|
crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_circle_center() {
|
|
||||||
let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
|
|
||||||
assert_eq!(actual[0], 5.0);
|
|
||||||
assert_eq!(actual[1], 0.0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,6 @@ mod tokeniser;
|
|||||||
|
|
||||||
// Re-export
|
// Re-export
|
||||||
pub use tokeniser::Input;
|
pub use tokeniser::Input;
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) use tokeniser::RESERVED_WORDS;
|
|
||||||
|
|
||||||
/// The types of tokens.
|
/// The types of tokens.
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, ts_rs::TS, JsonSchema, FromStr, Display)]
|
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, ts_rs::TS, JsonSchema, FromStr, Display)]
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use fnv::FnvHashMap;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use winnow::{
|
use winnow::{
|
||||||
ascii::{digit1, multispace1},
|
ascii::{digit1, multispace1},
|
||||||
combinator::{alt, opt, peek, preceded, repeat},
|
combinator::{alt, opt, peek, preceded, repeat, terminated},
|
||||||
error::{ContextError, ParseError},
|
error::{ContextError, ParseError},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
stream::{Location, Stream},
|
stream::{Location, Stream},
|
||||||
@ -15,52 +13,6 @@ use crate::{
|
|||||||
token::{Token, TokenType},
|
token::{Token, TokenType},
|
||||||
};
|
};
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub(crate) static ref RESERVED_WORDS: FnvHashMap<&'static str, TokenType> = {
|
|
||||||
let mut set = FnvHashMap::default();
|
|
||||||
set.insert("if", TokenType::Keyword);
|
|
||||||
set.insert("else", TokenType::Keyword);
|
|
||||||
set.insert("for", TokenType::Keyword);
|
|
||||||
set.insert("while", TokenType::Keyword);
|
|
||||||
set.insert("return", TokenType::Keyword);
|
|
||||||
set.insert("break", TokenType::Keyword);
|
|
||||||
set.insert("continue", TokenType::Keyword);
|
|
||||||
set.insert("fn", TokenType::Keyword);
|
|
||||||
set.insert("let", TokenType::Keyword);
|
|
||||||
set.insert("mut", TokenType::Keyword);
|
|
||||||
set.insert("as", TokenType::Keyword);
|
|
||||||
set.insert("loop", TokenType::Keyword);
|
|
||||||
set.insert("true", TokenType::Keyword);
|
|
||||||
set.insert("false", TokenType::Keyword);
|
|
||||||
set.insert("nil", TokenType::Keyword);
|
|
||||||
// This isn't a type because brackets are used for the type.
|
|
||||||
set.insert("array", TokenType::Keyword);
|
|
||||||
set.insert("and", TokenType::Keyword);
|
|
||||||
set.insert("or", TokenType::Keyword);
|
|
||||||
set.insert("not", TokenType::Keyword);
|
|
||||||
set.insert("var", TokenType::Keyword);
|
|
||||||
set.insert("const", TokenType::Keyword);
|
|
||||||
// "import" is special because of import().
|
|
||||||
set.insert("export", TokenType::Keyword);
|
|
||||||
set.insert("interface", TokenType::Keyword);
|
|
||||||
set.insert("new", TokenType::Keyword);
|
|
||||||
set.insert("self", TokenType::Keyword);
|
|
||||||
set.insert("record", TokenType::Keyword);
|
|
||||||
set.insert("struct", TokenType::Keyword);
|
|
||||||
set.insert("object", TokenType::Keyword);
|
|
||||||
set.insert("_", TokenType::Keyword);
|
|
||||||
|
|
||||||
set.insert("string", TokenType::Type);
|
|
||||||
set.insert("number", TokenType::Type);
|
|
||||||
set.insert("bool", TokenType::Type);
|
|
||||||
set.insert("sketch", TokenType::Type);
|
|
||||||
set.insert("sketch_surface", TokenType::Type);
|
|
||||||
set.insert("solid", TokenType::Type);
|
|
||||||
|
|
||||||
set
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lexer(i: &str, module_id: ModuleId) -> Result<Vec<Token>, ParseError<Input<'_>, ContextError>> {
|
pub fn lexer(i: &str, module_id: ModuleId) -> Result<Vec<Token>, ParseError<Input<'_>, ContextError>> {
|
||||||
let state = State::new(module_id);
|
let state = State::new(module_id);
|
||||||
let input = Input {
|
let input = Input {
|
||||||
@ -98,7 +50,7 @@ pub fn token(i: &mut Input<'_>) -> PResult<Token> {
|
|||||||
'$' => dollar,
|
'$' => dollar,
|
||||||
'!' => alt((operator, bang)),
|
'!' => alt((operator, bang)),
|
||||||
' ' | '\t' | '\n' => whitespace,
|
' ' | '\t' | '\n' => whitespace,
|
||||||
_ => alt((operator, keyword_type_or_word))
|
_ => alt((operator, keyword,type_, word))
|
||||||
}
|
}
|
||||||
.parse_next(i)
|
.parse_next(i)
|
||||||
{
|
{
|
||||||
@ -335,16 +287,47 @@ fn import_keyword(i: &mut Input<'_>) -> PResult<Token> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unambiguous_keyword_type_or_word(i: &mut Input<'_>) -> PResult<Token> {
|
fn unambiguous_keywords(i: &mut Input<'_>) -> PResult<Token> {
|
||||||
let mut w = word.parse_next(i)?;
|
// These are the keywords themselves.
|
||||||
if let Some(token_type) = RESERVED_WORDS.get(w.value.as_str()) {
|
let keyword_candidates = alt((
|
||||||
w.token_type = *token_type;
|
"if", "else", "for", "while", "return", "break", "continue", "fn", "let", "mut", "loop", "true", "false",
|
||||||
}
|
"nil", "and", "or", "not", "var", "const", "export",
|
||||||
Ok(w)
|
));
|
||||||
|
// Look ahead. If any of these characters follow the keyword, then it's not a keyword, it's just
|
||||||
|
// the start of a normal word.
|
||||||
|
let keyword = terminated(
|
||||||
|
keyword_candidates,
|
||||||
|
peek(none_of(('a'..='z', 'A'..='Z', '-', '_', '0'..='9'))),
|
||||||
|
);
|
||||||
|
let (value, range) = keyword.with_span().parse_next(i)?;
|
||||||
|
Ok(Token::from_range(
|
||||||
|
range,
|
||||||
|
i.state.module_id,
|
||||||
|
TokenType::Keyword,
|
||||||
|
value.to_owned(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_type_or_word(i: &mut Input<'_>) -> PResult<Token> {
|
fn keyword(i: &mut Input<'_>) -> PResult<Token> {
|
||||||
alt((import_keyword, unambiguous_keyword_type_or_word)).parse_next(i)
|
alt((import_keyword, unambiguous_keywords)).parse_next(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_(i: &mut Input<'_>) -> PResult<Token> {
|
||||||
|
// These are the types themselves.
|
||||||
|
let type_candidates = alt(("string", "number", "bool", "sketch", "sketch_surface", "solid"));
|
||||||
|
// Look ahead. If any of these characters follow the type, then it's not a type, it's just
|
||||||
|
// the start of a normal word.
|
||||||
|
let type_ = terminated(
|
||||||
|
type_candidates,
|
||||||
|
peek(none_of(('a'..='z', 'A'..='Z', '-', '_', '0'..='9'))),
|
||||||
|
);
|
||||||
|
let (value, range) = type_.with_span().parse_next(i)?;
|
||||||
|
Ok(Token::from_range(
|
||||||
|
range,
|
||||||
|
i.state.module_id,
|
||||||
|
TokenType::Type,
|
||||||
|
value.to_owned(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -1,157 +0,0 @@
|
|||||||
---
|
|
||||||
source: kcl/src/simulation_tests.rs
|
|
||||||
description: Result of parsing array_elem_push_fail.kcl
|
|
||||||
snapshot_kind: text
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"Ok": {
|
|
||||||
"body": [
|
|
||||||
{
|
|
||||||
"declarations": [
|
|
||||||
{
|
|
||||||
"end": 15,
|
|
||||||
"id": {
|
|
||||||
"end": 3,
|
|
||||||
"name": "arr",
|
|
||||||
"start": 0,
|
|
||||||
"type": "Identifier"
|
|
||||||
},
|
|
||||||
"init": {
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"end": 8,
|
|
||||||
"raw": "1",
|
|
||||||
"start": 7,
|
|
||||||
"type": "Literal",
|
|
||||||
"type": "Literal",
|
|
||||||
"value": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"end": 11,
|
|
||||||
"raw": "2",
|
|
||||||
"start": 10,
|
|
||||||
"type": "Literal",
|
|
||||||
"type": "Literal",
|
|
||||||
"value": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"end": 14,
|
|
||||||
"raw": "3",
|
|
||||||
"start": 13,
|
|
||||||
"type": "Literal",
|
|
||||||
"type": "Literal",
|
|
||||||
"value": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": 15,
|
|
||||||
"start": 6,
|
|
||||||
"type": "ArrayExpression",
|
|
||||||
"type": "ArrayExpression"
|
|
||||||
},
|
|
||||||
"start": 0,
|
|
||||||
"type": "VariableDeclarator"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": 15,
|
|
||||||
"kind": "const",
|
|
||||||
"start": 0,
|
|
||||||
"type": "VariableDeclaration",
|
|
||||||
"type": "VariableDeclaration"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"declarations": [
|
|
||||||
{
|
|
||||||
"end": 40,
|
|
||||||
"id": {
|
|
||||||
"end": 25,
|
|
||||||
"name": "pushedArr",
|
|
||||||
"start": 16,
|
|
||||||
"type": "Identifier"
|
|
||||||
},
|
|
||||||
"init": {
|
|
||||||
"arguments": [
|
|
||||||
{
|
|
||||||
"end": 36,
|
|
||||||
"name": "arr",
|
|
||||||
"start": 33,
|
|
||||||
"type": "Identifier",
|
|
||||||
"type": "Identifier"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"end": 39,
|
|
||||||
"raw": "4",
|
|
||||||
"start": 38,
|
|
||||||
"type": "Literal",
|
|
||||||
"type": "Literal",
|
|
||||||
"value": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"callee": {
|
|
||||||
"end": 32,
|
|
||||||
"name": "push",
|
|
||||||
"start": 28,
|
|
||||||
"type": "Identifier"
|
|
||||||
},
|
|
||||||
"end": 40,
|
|
||||||
"optional": false,
|
|
||||||
"start": 28,
|
|
||||||
"type": "CallExpression",
|
|
||||||
"type": "CallExpression"
|
|
||||||
},
|
|
||||||
"start": 16,
|
|
||||||
"type": "VariableDeclarator"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": 40,
|
|
||||||
"kind": "const",
|
|
||||||
"start": 16,
|
|
||||||
"type": "VariableDeclaration",
|
|
||||||
"type": "VariableDeclaration"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"declarations": [
|
|
||||||
{
|
|
||||||
"end": 54,
|
|
||||||
"id": {
|
|
||||||
"end": 45,
|
|
||||||
"name": "fail",
|
|
||||||
"start": 41,
|
|
||||||
"type": "Identifier"
|
|
||||||
},
|
|
||||||
"init": {
|
|
||||||
"computed": false,
|
|
||||||
"end": 54,
|
|
||||||
"object": {
|
|
||||||
"end": 51,
|
|
||||||
"name": "arr",
|
|
||||||
"start": 48,
|
|
||||||
"type": "Identifier",
|
|
||||||
"type": "Identifier"
|
|
||||||
},
|
|
||||||
"property": {
|
|
||||||
"end": 53,
|
|
||||||
"raw": "3",
|
|
||||||
"start": 52,
|
|
||||||
"type": "Literal",
|
|
||||||
"type": "Literal",
|
|
||||||
"value": 3
|
|
||||||
},
|
|
||||||
"start": 48,
|
|
||||||
"type": "MemberExpression",
|
|
||||||
"type": "MemberExpression"
|
|
||||||
},
|
|
||||||
"start": 41,
|
|
||||||
"type": "VariableDeclarator"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": 54,
|
|
||||||
"kind": "const",
|
|
||||||
"start": 41,
|
|
||||||
"type": "VariableDeclaration",
|
|
||||||
"type": "VariableDeclaration"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": 55,
|
|
||||||
"start": 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: kcl/src/simulation_tests.rs
|
|
||||||
description: Error from executing array_elem_push_fail.kcl
|
|
||||||
snapshot_kind: text
|
|
||||||
---
|
|
||||||
undefined value: KclErrorDetails { source_ranges: [SourceRange([48, 54, 0])], message: "The array doesn't have any item at index 3" }
|
|
@ -1,3 +0,0 @@
|
|||||||
arr = [1, 2, 3]
|
|
||||||
pushedArr = push(arr, 4)
|
|
||||||
fail = arr[3]
|
|
@ -1,219 +0,0 @@
|
|||||||
---
|
|
||||||
source: kcl/src/simulation_tests.rs
|
|
||||||
description: Result of tokenizing array_elem_push_fail.kcl
|
|
||||||
snapshot_kind: text
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"Ok": [
|
|
||||||
{
|
|
||||||
"type": "word",
|
|
||||||
"start": 0,
|
|
||||||
"end": 3,
|
|
||||||
"value": "arr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 3,
|
|
||||||
"end": 4,
|
|
||||||
"value": " "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "operator",
|
|
||||||
"start": 4,
|
|
||||||
"end": 5,
|
|
||||||
"value": "="
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 5,
|
|
||||||
"end": 6,
|
|
||||||
"value": " "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "brace",
|
|
||||||
"start": 6,
|
|
||||||
"end": 7,
|
|
||||||
"value": "["
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "number",
|
|
||||||
"start": 7,
|
|
||||||
"end": 8,
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "comma",
|
|
||||||
"start": 8,
|
|
||||||
"end": 9,
|
|
||||||
"value": ","
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 9,
|
|
||||||
"end": 10,
|
|
||||||
"value": " "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "number",
|
|
||||||
"start": 10,
|
|
||||||
"end": 11,
|
|
||||||
"value": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "comma",
|
|
||||||
"start": 11,
|
|
||||||
"end": 12,
|
|
||||||
"value": ","
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 12,
|
|
||||||
"end": 13,
|
|
||||||
"value": " "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "number",
|
|
||||||
"start": 13,
|
|
||||||
"end": 14,
|
|
||||||
"value": "3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "brace",
|
|
||||||
"start": 14,
|
|
||||||
"end": 15,
|
|
||||||
"value": "]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 15,
|
|
||||||
"end": 16,
|
|
||||||
"value": "\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "word",
|
|
||||||
"start": 16,
|
|
||||||
"end": 25,
|
|
||||||
"value": "pushedArr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 25,
|
|
||||||
"end": 26,
|
|
||||||
"value": " "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "operator",
|
|
||||||
"start": 26,
|
|
||||||
"end": 27,
|
|
||||||
"value": "="
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 27,
|
|
||||||
"end": 28,
|
|
||||||
"value": " "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "word",
|
|
||||||
"start": 28,
|
|
||||||
"end": 32,
|
|
||||||
"value": "push"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "brace",
|
|
||||||
"start": 32,
|
|
||||||
"end": 33,
|
|
||||||
"value": "("
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "word",
|
|
||||||
"start": 33,
|
|
||||||
"end": 36,
|
|
||||||
"value": "arr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "comma",
|
|
||||||
"start": 36,
|
|
||||||
"end": 37,
|
|
||||||
"value": ","
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 37,
|
|
||||||
"end": 38,
|
|
||||||
"value": " "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "number",
|
|
||||||
"start": 38,
|
|
||||||
"end": 39,
|
|
||||||
"value": "4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "brace",
|
|
||||||
"start": 39,
|
|
||||||
"end": 40,
|
|
||||||
"value": ")"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 40,
|
|
||||||
"end": 41,
|
|
||||||
"value": "\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "word",
|
|
||||||
"start": 41,
|
|
||||||
"end": 45,
|
|
||||||
"value": "fail"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 45,
|
|
||||||
"end": 46,
|
|
||||||
"value": " "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "operator",
|
|
||||||
"start": 46,
|
|
||||||
"end": 47,
|
|
||||||
"value": "="
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 47,
|
|
||||||
"end": 48,
|
|
||||||
"value": " "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "word",
|
|
||||||
"start": 48,
|
|
||||||
"end": 51,
|
|
||||||
"value": "arr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "brace",
|
|
||||||
"start": 51,
|
|
||||||
"end": 52,
|
|
||||||
"value": "["
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "number",
|
|
||||||
"start": 52,
|
|
||||||
"end": 53,
|
|
||||||
"value": "3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "brace",
|
|
||||||
"start": 53,
|
|
||||||
"end": 54,
|
|
||||||
"value": "]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "whitespace",
|
|
||||||
"start": 54,
|
|
||||||
"end": 55,
|
|
||||||
"value": "\n"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
---
|
|
||||||
source: kcl/src/simulation_tests.rs
|
|
||||||
description: Result of parsing array_index_oob.kcl
|
|
||||||
snapshot_kind: text
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"Ok": {
|
|
||||||
"body": [
|
|
||||||
{
|
|
||||||
"declarations": [
|
|
||||||
{
|
|
||||||
"end": 8,
|
|
||||||
"id": {
|
|
||||||
"end": 3,
|
|
||||||
"name": "arr",
|
|
||||||
"start": 0,
|
|
||||||
"type": "Identifier"
|
|
||||||
},
|
|
||||||
"init": {
|
|
||||||
"elements": [],
|
|
||||||
"end": 8,
|
|
||||||
"start": 6,
|
|
||||||
"type": "ArrayExpression",
|
|
||||||
"type": "ArrayExpression"
|
|
||||||
},
|
|
||||||
"start": 0,
|
|
||||||
"type": "VariableDeclarator"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": 8,
|
|
||||||
"kind": "const",
|
|
||||||
"start": 0,
|
|
||||||
"type": "VariableDeclaration",
|
|
||||||
"type": "VariableDeclaration"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"declarations": [
|
|
||||||
{
|
|
||||||
"end": 19,
|
|
||||||
"id": {
|
|
||||||
"end": 10,
|
|
||||||
"name": "x",
|
|
||||||
"start": 9,
|
|
||||||
"type": "Identifier"
|
|
||||||
},
|
|
||||||
"init": {
|
|
||||||
"computed": false,
|
|
||||||
"end": 19,
|
|
||||||
"object": {
|
|
||||||
"end": 16,
|
|
||||||
"name": "arr",
|
|
||||||
"start": 13,
|
|
||||||
"type": "Identifier",
|
|
||||||
"type": "Identifier"
|
|
||||||
},
|
|
||||||
"property": {
|
|
||||||
"end": 18,
|
|
||||||
"raw": "0",
|
|
||||||
"start": 17,
|
|
||||||
"type": "Literal",
|
|
||||||
"type": "Literal",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
"start": 13,
|
|
||||||
"type": "MemberExpression",
|
|
||||||
"type": "MemberExpression"
|
|
||||||
},
|
|
||||||
"start": 9,
|
|
||||||
"type": "VariableDeclarator"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": 19,
|
|
||||||
"kind": "const",
|
|
||||||
"start": 9,
|
|
||||||
"type": "VariableDeclaration",
|
|
||||||
"type": "VariableDeclaration"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": 20,
|
|
||||||
"start": 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: kcl/src/simulation_tests.rs
|
|
||||||
description: Error from executing array_index_oob.kcl
|
|
||||||
snapshot_kind: text
|
|
||||||
---
|
|
||||||
undefined value: KclErrorDetails { source_ranges: [SourceRange([13, 19, 0])], message: "The array doesn't have any item at index 0" }
|
|