Add 3 point arc (#5722)
* bare minimum * start of segment util added * remove redundant handle * some probably buggy handling of arc handles, can fix later * probably bug implementation of update args, but data flow through is mostly there can fix bugs after * fix update for arc * fix math for center handle * fix up length indicator * tweak math * stub out xState logic for arc * more progress on adding point and click, implemented more of sketchLineHelper for arc * small unrelated tweak * fix up draft arc bugs * fix arc last click * fix draft segment animation and add comment * add draft point snapping for arcs * add helper stuff to arc * clone arc point and click as base for arc-three-point * rust change for arc three point * can draw three point arc * make arcTo editable * can add new three point arc, so long as it continues existing profile * get overlays working * make snap to for continuing profile work for three point arcs * add draft animation * tangent issue fix * action rename * tmp test fix up * fix silly bug * fix couple problems causing tests to fail * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * fix up * add delet segment test for new segments * update docs * draft segments should look right * add test for dragging new segment handles * arc tools can be chained now * make three point arc can start a new profile (not only extend existing paths) * add test for equiping and unequiping the tool plus drawing with it * fix console noise * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * clean up * update rust/docs * put toolbar mode check into fixture * do thing for lee * use TEST_COLORSs * fix colors * don't await file write * remove commented code * remove unneeded template strings * power to **2 * remove magic numbers * more string templates * some odd bits of clean up * arc should be enable in dev * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * add new simulation test * fix test code from kwark migration * issues Frank found * fix deleting half complete ark * fix * small fix on dele index * tsc post main merge * fix up snaping to profile start * add cross hari for three point arc * block snapping if it's the only segment * add tests for canceling arcTo halfway through --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
17296
docs/kcl/std.json
@ -126,6 +126,30 @@ A base path.
|
||||
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
|
||||
|
||||
|
||||
----
|
||||
A base path.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `ArcThreePoint`| | No |
|
||||
| `p1` |`[number, number]`| Point 1 of the arc (base on the end of previous segment) | No |
|
||||
| `p2` |`[number, number]`| Point 2 of the arc (interior kwarg) | No |
|
||||
| `p3` |`[number, number]`| Point 3 of the arc (end kwarg) | No |
|
||||
| `from` |`[number, number]`| The from point. | No |
|
||||
| `to` |`[number, number]`| The to point. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
|
||||
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No |
|
||||
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
|
||||
|
||||
|
||||
----
|
||||
A path that is horizontal.
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
} from '../test-utils'
|
||||
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
|
||||
import { SIDEBAR_BUTTON_SUFFIX } from 'lib/constants'
|
||||
import { ToolbarModeName } from 'lib/toolbar'
|
||||
|
||||
export class ToolbarFixture {
|
||||
public page: Page
|
||||
@ -120,6 +121,15 @@ export class ToolbarFixture {
|
||||
// this is for the engine animation, as it takes 500ms to complete
|
||||
await this.page.waitForTimeout(600)
|
||||
}
|
||||
private _getMode = () =>
|
||||
this.page.locator('[data-current-mode]').getAttribute('data-current-mode')
|
||||
expectToolbarMode = {
|
||||
toBe: (mode: ToolbarModeName) => expect.poll(this._getMode).toEqual(mode),
|
||||
not: {
|
||||
toBe: (mode: ToolbarModeName) =>
|
||||
expect.poll(this._getMode).not.toEqual(mode),
|
||||
},
|
||||
}
|
||||
|
||||
private _serialiseFileTree = async () => {
|
||||
return this.page
|
||||
@ -176,6 +186,22 @@ export class ToolbarFixture {
|
||||
).toBeVisible()
|
||||
await this.page.getByTestId('dropdown-circle-three-points').click()
|
||||
}
|
||||
selectArc = async () => {
|
||||
await this.page
|
||||
.getByRole('button', { name: 'caret down Tangential Arc:' })
|
||||
.click()
|
||||
await expect(this.page.getByTestId('dropdown-arc')).toBeVisible()
|
||||
await this.page.getByTestId('dropdown-arc').click()
|
||||
}
|
||||
selectThreePointArc = async () => {
|
||||
await this.page
|
||||
.getByRole('button', { name: 'caret down Tangential Arc:' })
|
||||
.click()
|
||||
await expect(
|
||||
this.page.getByTestId('dropdown-three-point-arc')
|
||||
).toBeVisible()
|
||||
await this.page.getByTestId('dropdown-three-point-arc').click()
|
||||
}
|
||||
|
||||
async closePane(paneId: SidebarType) {
|
||||
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)
|
||||
|
@ -53,7 +53,7 @@ sketch003 = startSketchOn('XY')
|
||||
|> close()
|
||||
extrude003 = extrude(sketch003, length = 20)
|
||||
`
|
||||
|
||||
test.describe('edit with AI example snapshots', () => {
|
||||
test(
|
||||
`change colour`,
|
||||
{ tag: '@snapshot' },
|
||||
@ -96,3 +96,4 @@ test(
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -319,7 +319,6 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
'when engine fails export we handle the failure and alert the user',
|
||||
{ tag: '@skipLocalEngine' },
|
||||
async ({ scene, page, homePage, cmdBar }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(
|
||||
async ({ code }) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
@ -636,11 +635,8 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
await homePage.goToModelingScene()
|
||||
})
|
||||
|
||||
const toolBarMode = () =>
|
||||
page.locator('[data-currentMode]').getAttribute('data-currentMode')
|
||||
|
||||
await test.step('Start sketch and select a plane', async () => {
|
||||
await expect.poll(toolBarMode).toEqual('modeling')
|
||||
await toolbar.expectToolbarMode.toBe('modeling')
|
||||
// Click the start sketch button
|
||||
await toolbar.startSketchPlaneSelection()
|
||||
|
||||
@ -649,10 +645,10 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
|
||||
// Check that the modeling toolbar doesn't appear during the animation
|
||||
// The animation typically takes around 500ms, so we'll check for a second
|
||||
await expect.poll(toolBarMode, { timeout: 1000 }).not.toEqual('modeling')
|
||||
await toolbar.expectToolbarMode.not.toBe('modeling')
|
||||
|
||||
// After animation completes, we should see the sketching toolbar
|
||||
await expect.poll(toolBarMode).toEqual('sketching')
|
||||
await toolbar.expectToolbarMode.toBe('sketching')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
} from './test-utils'
|
||||
import { uuidv4, roundOff } from 'lib/utils'
|
||||
import { SceneFixture } from './fixtures/sceneFixture'
|
||||
import { CmdBarFixture } from './fixtures/cmdBarFixture'
|
||||
|
||||
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
||||
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
|
||||
@ -191,7 +192,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
page: Page,
|
||||
homePage: HomePageFixture,
|
||||
openPanes: string[],
|
||||
scene: SceneFixture
|
||||
scene: SceneFixture,
|
||||
cmdBar: CmdBarFixture
|
||||
) => {
|
||||
// Load the app with the code panes
|
||||
await page.addInitScript(async () => {
|
||||
@ -201,13 +203,22 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
|> startProfileAt([4.61, -14.01], %)
|
||||
|> line(end = [12.73, -0.09])
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> arcTo({
|
||||
interior = [20.18, -1.7],
|
||||
end = [11.82, -1.16]
|
||||
}, %)
|
||||
|> arc({
|
||||
radius = 5.92,
|
||||
angleStart = -89.36,
|
||||
angleEnd = 135.81
|
||||
}, %)
|
||||
|> close()`
|
||||
)
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
@ -242,7 +253,17 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
|> startProfileAt([4.61, -14.01], %)
|
||||
|> line(end = [12.73, -0.09])
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> close()`)
|
||||
|> arcTo({
|
||||
interior = [20.18, -1.7],
|
||||
end = [11.82, -1.16]
|
||||
}, %)
|
||||
|> arc({
|
||||
radius = 5.92,
|
||||
angleStart = -89.36,
|
||||
angleEnd = 135.81
|
||||
}, %)
|
||||
|> close()
|
||||
`)
|
||||
} else {
|
||||
// Ensure we don't see the code.
|
||||
await expect(u.codeLocator).not.toBeVisible()
|
||||
@ -272,7 +293,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
|
||||
const step5 = { steps: 5 }
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(5)
|
||||
|
||||
// drag startProfileAt handle
|
||||
await page.mouse.move(startPX[0], startPX[1])
|
||||
@ -310,6 +331,68 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
}
|
||||
|
||||
// drag arcTo interior handle (three point arc)
|
||||
const arcToHandle = await u.getBoundingBox('[data-overlay-index="2"]')
|
||||
await page.mouse.move(arcToHandle.x, arcToHandle.y - 5)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(
|
||||
arcToHandle.x - dragPX,
|
||||
arcToHandle.y + dragPX,
|
||||
step5
|
||||
)
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(100)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
}
|
||||
|
||||
// drag arcTo end handle (three point arc)
|
||||
const arcToEndHandle = await u.getBoundingBox('[data-overlay-index="3"]')
|
||||
await page.mouse.move(arcToEndHandle.x, arcToEndHandle.y - 5)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(
|
||||
arcToEndHandle.x - dragPX,
|
||||
arcToEndHandle.y + dragPX,
|
||||
step5
|
||||
)
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(100)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
}
|
||||
|
||||
// drag arc radius handle
|
||||
const arcRadiusHandle = await u.getBoundingBox('[data-overlay-index="4"]')
|
||||
await page.mouse.move(arcRadiusHandle.x, arcRadiusHandle.y - 5)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(
|
||||
arcRadiusHandle.x - dragPX,
|
||||
arcRadiusHandle.y + dragPX,
|
||||
step5
|
||||
)
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(100)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
}
|
||||
|
||||
// drag arc center handle (we'll have to hardcode the position because it doesn't have a overlay near the handle)
|
||||
const arcCenterHandle = { x: 745, y: 214 }
|
||||
await page.mouse.move(arcCenterHandle.x, arcCenterHandle.y - 5)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(
|
||||
arcCenterHandle.x - dragPX,
|
||||
arcCenterHandle.y + dragPX,
|
||||
step5
|
||||
)
|
||||
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
|
||||
await u.openKclCodePanel()
|
||||
|
||||
@ -318,14 +401,23 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([6.44, -12.07], %)
|
||||
|> line(end = [14.72, 1.97])
|
||||
|> tangentialArcTo([24.95, -5.38], %)
|
||||
|> line(end = [1.97, 2.06])
|
||||
|> close()`)
|
||||
|> tangentialArcTo([26.92, -3.32], %)
|
||||
|> arcTo({
|
||||
interior = [18.11, -3.73],
|
||||
end = [9.77, -3.19]
|
||||
}, %)
|
||||
|> arc({
|
||||
radius = 3.75,
|
||||
angleStart = -58.29,
|
||||
angleEnd = 161.17
|
||||
}, %)
|
||||
|> close()
|
||||
`)
|
||||
}
|
||||
test(
|
||||
'code pane open at start-handles',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page, homePage, scene }) => {
|
||||
async ({ page, homePage, scene, cmdBar }) => {
|
||||
// Load the app with the code panes
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -338,14 +430,20 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
})
|
||||
)
|
||||
})
|
||||
await doEditSegmentsByDraggingHandle(page, homePage, ['code'], scene)
|
||||
await doEditSegmentsByDraggingHandle(
|
||||
page,
|
||||
homePage,
|
||||
['code'],
|
||||
scene,
|
||||
cmdBar
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'code pane closed at start-handles',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page, homePage, scene }) => {
|
||||
async ({ page, homePage, scene, cmdBar }) => {
|
||||
// Load the app with the code panes
|
||||
await page.addInitScript(async (persistModelingContext) => {
|
||||
localStorage.setItem(
|
||||
@ -353,7 +451,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
JSON.stringify({ openPanes: [] })
|
||||
)
|
||||
}, PERSIST_MODELING_CONTEXT)
|
||||
await doEditSegmentsByDraggingHandle(page, homePage, [], scene)
|
||||
await doEditSegmentsByDraggingHandle(page, homePage, [], scene, cmdBar)
|
||||
}
|
||||
)
|
||||
})
|
||||
@ -362,6 +460,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
page,
|
||||
editor,
|
||||
homePage,
|
||||
scene,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
@ -373,6 +473,8 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
})
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
@ -1355,7 +1457,7 @@ test.describe('multi-profile sketching', () => {
|
||||
test(
|
||||
`test it removes half-finished expressions when changing tools in sketch mode`,
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ context, page, scene, toolbar, editor, homePage }) => {
|
||||
async ({ context, page, scene, toolbar, editor, homePage, cmdBar }) => {
|
||||
// We seed the scene with a single offset plane
|
||||
await context.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
@ -1375,7 +1477,10 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
|
||||
)
|
||||
})
|
||||
|
||||
const [continueProfile2Clk] = scene.makeMouseHelpers(954, 282)
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
@ -1386,7 +1491,13 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
|
||||
const [circlePoint1] = scene.makeMouseHelpers(700, 200)
|
||||
|
||||
await test.step('equip circle tool and click first point', async () => {
|
||||
// await page.waitForTimeout(100)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
await toolbar.circleBtn.click()
|
||||
return toolbar.circleBtn.getAttribute('aria-pressed')
|
||||
})
|
||||
.toBe('true')
|
||||
await page.waitForTimeout(100)
|
||||
await circlePoint1()
|
||||
await editor.expectEditor.toContain(
|
||||
@ -1401,6 +1512,7 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
|
||||
|
||||
const [circle3Point1] = scene.makeMouseHelpers(650, 200)
|
||||
const [circle3Point2] = scene.makeMouseHelpers(750, 200)
|
||||
// const [circle3Point3] = scene.makeMouseHelpers(700, 150)
|
||||
|
||||
await test.step('equip three point circle tool and click first two points', async () => {
|
||||
await toolbar.selectCircleThreePoint()
|
||||
@ -1411,25 +1523,40 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
|
||||
await editor.expectEditor.toContain('profile003 = circleThreePoint(')
|
||||
})
|
||||
|
||||
await test.step('equip line tool and verify three point circle code is removed', async () => {
|
||||
await test.step('equip line tool and verify three-point circle code is removed', async () => {
|
||||
await toolbar.lineBtn.click()
|
||||
await editor.expectEditor.not.toContain(
|
||||
'profile003 = circleThreePoint('
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('equip three-point-arc tool and click first two points', async () => {
|
||||
await page.waitForTimeout(200)
|
||||
await toolbar.selectThreePointArc()
|
||||
await page.waitForTimeout(200)
|
||||
await circle3Point1()
|
||||
await page.waitForTimeout(200)
|
||||
await circle3Point2()
|
||||
await editor.expectEditor.toContain('arcTo({')
|
||||
})
|
||||
|
||||
await test.step('equip line tool and verify three-point-arc code is removed after second click', async () => {
|
||||
await toolbar.lineBtn.click()
|
||||
await editor.expectEditor.not.toContain('arcTo({')
|
||||
})
|
||||
|
||||
const [cornerRectPoint1] = scene.makeMouseHelpers(600, 300)
|
||||
|
||||
await test.step('equip corner rectangle tool and click first point', async () => {
|
||||
await toolbar.rectangleBtn.click()
|
||||
await page.waitForTimeout(100)
|
||||
await cornerRectPoint1()
|
||||
await editor.expectEditor.toContain('profile003 = startProfileAt(')
|
||||
await editor.expectEditor.toContain('profile004 = startProfileAt(')
|
||||
})
|
||||
|
||||
await test.step('equip line tool and verify corner rectangle code is removed', async () => {
|
||||
await toolbar.lineBtn.click()
|
||||
await editor.expectEditor.not.toContain('profile003 = startProfileAt(')
|
||||
await editor.expectEditor.not.toContain('profile004 = startProfileAt(')
|
||||
})
|
||||
|
||||
const [centerRectPoint1] = scene.makeMouseHelpers(700, 300)
|
||||
@ -1438,12 +1565,24 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
|
||||
await toolbar.selectCenterRectangle()
|
||||
await page.waitForTimeout(100)
|
||||
await centerRectPoint1()
|
||||
await editor.expectEditor.toContain('profile003 = startProfileAt(')
|
||||
await editor.expectEditor.toContain('profile004 = startProfileAt(')
|
||||
})
|
||||
|
||||
await test.step('equip line tool and verify center rectangle code is removed', async () => {
|
||||
await toolbar.lineBtn.click()
|
||||
await editor.expectEditor.not.toContain('profile003 = startProfileAt(')
|
||||
await editor.expectEditor.not.toContain('profile004 = startProfileAt(')
|
||||
})
|
||||
|
||||
await test.step('continue profile002 with the three point arc tool, and then switch back to the line tool to verify it only removes the last expression in the pipe', async () => {
|
||||
await toolbar.selectThreePointArc()
|
||||
await page.waitForTimeout(200)
|
||||
await continueProfile2Clk()
|
||||
await page.waitForTimeout(200)
|
||||
await circle3Point1()
|
||||
await editor.expectEditor.toContain('arcTo({')
|
||||
await toolbar.lineBtn.click()
|
||||
await editor.expectEditor.not.toContain('arcTo({')
|
||||
await editor.expectEditor.toContain('profile002')
|
||||
})
|
||||
}
|
||||
)
|
||||
@ -1532,6 +1671,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
|
||||
}) => {
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.connectionEstablished()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
@ -1595,7 +1735,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
|
||||
// timeout wait for engine animation is unavoidable
|
||||
await page.waitForTimeout(600)
|
||||
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
|
||||
await test.step('Create a close profile stopping mid profile to equip the tangential arc, and than back to the line tool', async () => {
|
||||
await test.step('Create a close profile stopping mid profile to equip the tangential arc, then three-point arc, and then back to the line tool', async () => {
|
||||
await startProfile1()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile001 = startProfileAt([4.61, 12.21], sketch001)`
|
||||
@ -1613,12 +1753,45 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
|
||||
await editor.expectEditor.toContain(
|
||||
`|> tangentialArcTo([16.61, 4.14], %)`
|
||||
)
|
||||
|
||||
// Add a three-point arc segment
|
||||
await toolbar.selectThreePointArc()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// select end of profile again
|
||||
await endLineStartTanArc()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Define points for the three-point arc
|
||||
const [threePointInterior, threePointInteriorMove] =
|
||||
scene.makeMouseHelpers(600, 200)
|
||||
const [threePointEnd, threePointEndMove] = scene.makeMouseHelpers(
|
||||
590,
|
||||
270
|
||||
)
|
||||
|
||||
// Create the three-point arc
|
||||
await page.waitForTimeout(300)
|
||||
await threePointInteriorMove()
|
||||
await threePointInterior()
|
||||
await page.waitForTimeout(300)
|
||||
await threePointEndMove()
|
||||
await threePointEnd()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Verify the three-point arc was created correctly
|
||||
await editor.expectEditor.toContain(`|> arcTo(`)
|
||||
|
||||
// Switch back to line tool to continue
|
||||
await toolbar.lineBtn.click()
|
||||
await page.waitForTimeout(300)
|
||||
await endArcStartLine()
|
||||
|
||||
// Continue with the original line segment
|
||||
await threePointEnd()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
await page.mouse.click(572, 110)
|
||||
await editor.expectEditor.toContain(`|> line(end = [-11.73, 5.35])`)
|
||||
await editor.expectEditor.toContain(`|> line(end = [-1.22, 10.85])`)
|
||||
await startProfile1()
|
||||
await editor.expectEditor.toContain(
|
||||
`|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
@ -1820,8 +1993,68 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('double check that circle three point can be unequiped', async () => {
|
||||
// this was tested implicitly for other tools, but not for circle three point since it's last
|
||||
await test.step('create three-point arcs in a row without an unequip', async () => {
|
||||
// Define points for the first three-point arc
|
||||
const [arc1Point1, arc1Point1Move] = scene.makeMouseHelpers(700, 397)
|
||||
const [arc1Point2, arc1Point2Move] = scene.makeMouseHelpers(724, 346)
|
||||
const [arc1Point3, arc1Point3Move] = scene.makeMouseHelpers(785, 415)
|
||||
|
||||
// Define points for the second three-point arc
|
||||
const [arc2Point1, arc2Point1Move] = scene.makeMouseHelpers(792, 225)
|
||||
const [arc2Point2, arc2Point2Move] = scene.makeMouseHelpers(820, 207)
|
||||
const [arc2Point3, arc2Point3Move] = scene.makeMouseHelpers(905, 229)
|
||||
|
||||
// Select the three-point arc tool
|
||||
await toolbar.selectThreePointArc()
|
||||
|
||||
// Create the first three-point arc
|
||||
await arc1Point1Move()
|
||||
await arc1Point1()
|
||||
await page.waitForTimeout(300)
|
||||
await arc1Point2Move()
|
||||
await arc1Point2()
|
||||
await page.waitForTimeout(300)
|
||||
await arc1Point3Move()
|
||||
await arc1Point3()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Verify the first three-point arc was created correctly
|
||||
await editor.expectEditor.toContain(
|
||||
`profile011 = startProfileAt([13.56, -9.97], sketch001)
|
||||
|> arcTo({
|
||||
interior = [15.19, -6.51],
|
||||
end = [19.33, -11.19]
|
||||
}, %)`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
|
||||
// Create the second three-point arc
|
||||
await arc2Point1Move()
|
||||
await arc2Point1()
|
||||
await page.waitForTimeout(300)
|
||||
await arc2Point2Move()
|
||||
await arc2Point2()
|
||||
await page.waitForTimeout(300)
|
||||
await arc2Point3Move()
|
||||
await arc2Point3()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Verify the second three-point arc was created correctly
|
||||
await editor.expectEditor.toContain(
|
||||
` |> arcTo({
|
||||
interior = [19.8, 1.7],
|
||||
end = [21.7, 2.92]
|
||||
}, %)
|
||||
|> arcTo({
|
||||
interior = [27.47, 1.42],
|
||||
end = [27.57, 1.52]
|
||||
}, %)`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
})
|
||||
|
||||
await test.step('double check that three-point arc can be unequipped', async () => {
|
||||
// this was tested implicitly for other tools, but not for three-point arc since it's last
|
||||
await page.waitForTimeout(300)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@ -2085,7 +2318,7 @@ profile003 = circle(sketch001, center = [6.92, -4.2], radius = 3.16)
|
||||
test(
|
||||
'can enter sketch when there is an extrude',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ homePage, scene, toolbar, page }) => {
|
||||
async ({ homePage, scene, toolbar, page, cmdBar }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
@ -2122,6 +2355,8 @@ extrude001 = extrude(profile003, length = 5)
|
||||
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
@ -2134,9 +2369,11 @@ extrude001 = extrude(profile003, length = 5)
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
await test.step('check the sketch is still drawn properly', async () => {
|
||||
await scene.expectPixelColor([255, 255, 255], { x: 596, y: 165 }, 15)
|
||||
await scene.expectPixelColor([255, 255, 255], { x: 641, y: 220 }, 15)
|
||||
await scene.expectPixelColor([255, 255, 255], { x: 763, y: 214 }, 15)
|
||||
await Promise.all([
|
||||
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 596, y: 165 }, 15),
|
||||
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 641, y: 220 }, 15),
|
||||
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 763, y: 214 }, 15),
|
||||
])
|
||||
})
|
||||
}
|
||||
)
|
||||
@ -2293,7 +2530,7 @@ extrude001 = extrude(thePart, length = 75)
|
||||
test(
|
||||
'Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ homePage, scene, toolbar, editor, page }) => {
|
||||
async ({ homePage, scene, toolbar, editor, page, cmdBar }) => {
|
||||
// TODO this test should include a test for selecting revolve walls and caps
|
||||
|
||||
await page.addInitScript(async () => {
|
||||
@ -2378,6 +2615,8 @@ extrude003 = extrude(profile011, length = 2.5)
|
||||
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
@ -2440,39 +2679,22 @@ extrude003 = extrude(profile011, length = 2.5)
|
||||
|
||||
const verifyWallProfilesAreDrawn = async () =>
|
||||
test.step('verify wall profiles are drawn', async () => {
|
||||
await Promise.all([
|
||||
// open polygon
|
||||
await scene.expectPixelColor(
|
||||
TEST_COLORS.WHITE,
|
||||
{ x: 599, y: 168 },
|
||||
15
|
||||
)
|
||||
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 599, y: 168 }, 15),
|
||||
// closed polygon
|
||||
await scene.expectPixelColor(
|
||||
TEST_COLORS.WHITE,
|
||||
{ x: 656, y: 171 },
|
||||
15
|
||||
)
|
||||
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 656, y: 171 }, 15),
|
||||
// revolved profile
|
||||
await scene.expectPixelColor(
|
||||
TEST_COLORS.WHITE,
|
||||
{ x: 655, y: 264 },
|
||||
15
|
||||
)
|
||||
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 655, y: 264 }, 15),
|
||||
// extruded profile
|
||||
await scene.expectPixelColor(
|
||||
TEST_COLORS.WHITE,
|
||||
{ x: 808, y: 396 },
|
||||
15
|
||||
)
|
||||
// circle
|
||||
await scene.expectPixelColor(
|
||||
[
|
||||
TEST_COLORS.WHITE,
|
||||
TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue
|
||||
],
|
||||
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 808, y: 396 }, 15),
|
||||
// circle (When entering via the circle, it's selected and therefore blue)
|
||||
scene.expectPixelColor(
|
||||
[TEST_COLORS.WHITE, TEST_COLORS.BLUE],
|
||||
{ x: 742, y: 386 },
|
||||
15
|
||||
)
|
||||
),
|
||||
])
|
||||
})
|
||||
|
||||
const verifyCapProfilesAreDrawn = async () =>
|
||||
|
@ -410,9 +410,9 @@ test.describe(
|
||||
test(
|
||||
'Draft segments should look right',
|
||||
{ tag: '@snapshot' },
|
||||
async ({ page, context, scene, cmdBar }) => {
|
||||
async ({ page, scene, toolbar }) => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
// test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
@ -421,6 +421,23 @@ test(
|
||||
|
||||
await scene.connectionEstablished()
|
||||
|
||||
const startXPx = 600
|
||||
const [endOfTangentClk, endOfTangentMv] = scene.makeMouseHelpers(
|
||||
startXPx + PUR * 30,
|
||||
500 - PUR * 20,
|
||||
{ steps: 10 }
|
||||
)
|
||||
const [threePointArcMidPointClk, threePointArcMidPointMv] =
|
||||
scene.makeMouseHelpers(800, 250, { steps: 10 })
|
||||
const [threePointArcEndPointClk, threePointArcEndPointMv] =
|
||||
scene.makeMouseHelpers(750, 285, { steps: 10 })
|
||||
const [arcCenterClk, arcCenterMv] = scene.makeMouseHelpers(750, 210, {
|
||||
steps: 10,
|
||||
})
|
||||
const [arcEndClk, arcEndMv] = scene.makeMouseHelpers(750, 150, {
|
||||
steps: 10,
|
||||
})
|
||||
|
||||
// click on "Start Sketch" button
|
||||
await u.doAndWaitForImageDiff(
|
||||
() => page.getByRole('button', { name: 'Start Sketch' }).click(),
|
||||
@ -435,7 +452,6 @@ test(
|
||||
|
||||
await page.waitForTimeout(700) // TODO detect animation ending, or disable animation
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
@ -471,12 +487,52 @@ test(
|
||||
await page.mouse.move(813, 392, { steps: 10 })
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
|
||||
await endOfTangentMv()
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
})
|
||||
await endOfTangentClk()
|
||||
|
||||
await toolbar.selectThreePointArc()
|
||||
await page.waitForTimeout(500)
|
||||
await endOfTangentClk()
|
||||
await threePointArcMidPointMv()
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
})
|
||||
await threePointArcMidPointClk()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await threePointArcEndPointMv()
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
})
|
||||
|
||||
await threePointArcEndPointClk()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await toolbar.selectArc()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// continue the profile
|
||||
await threePointArcEndPointClk()
|
||||
await page.waitForTimeout(100)
|
||||
await arcCenterMv()
|
||||
await page.waitForTimeout(500)
|
||||
await arcCenterClk()
|
||||
|
||||
await arcEndMv()
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
})
|
||||
await arcEndClk()
|
||||
}
|
||||
)
|
||||
|
||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 69 KiB |
@ -0,0 +1,33 @@
|
||||
{
|
||||
"original_source_code": "sketch001 = startSketchOn('XZ')\nprofile001 = startProfileAt([57.81, 250.51], sketch001)\n |> line(end = [121.13, 56.63], tag = $seg02)\n |> line(end = [83.37, -34.61], tag = $seg01)\n |> line(end = [19.66, -116.4])\n |> line(end = [-221.8, -41.69])\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude001 = extrude(profile001, length = 200)\nsketch002 = startSketchOn('XZ')\n |> startProfileAt([-73.64, -42.89], %)\n |> xLine(length = 173.71)\n |> line(end = [-22.12, -94.4])\n |> xLine(length = -156.98)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude002 = extrude(sketch002, length = 50)\nsketch003 = startSketchOn('XY')\n |> startProfileAt([52.92, 157.81], %)\n |> angledLine([0, 176.4], %, $rectangleSegmentA001)\n |> angledLine([\n segAng(rectangleSegmentA001) - 90,\n 53.4\n ], %, $rectangleSegmentB001)\n |> angledLine([\n segAng(rectangleSegmentA001),\n -segLen(rectangleSegmentA001)\n ], %, $rectangleSegmentC001)\n |> line(endAbsolute = [profileStartX(%), profileStartY(%)])\n |> close()\nextrude003 = extrude(sketch003, length = 20)\n",
|
||||
"prompt": "make this neon green please, use #39FF14",
|
||||
"source_ranges": [
|
||||
{
|
||||
"prompt": "The users main selection is the end cap of a general-sweep (that is an extrusion, revolve, sweep or loft).\nThe source range most likely refers to \"startProfileAt\" simply because this is the start of the profile that was swept.\nIf you need to operate on this cap, for example for sketching on the face, you can use the special string END i.e. `startSketchOn(someSweepVariable, END)`\nWhen they made this selection they main have intended this surface directly or meant something more general like the sweep body.\nSee later source ranges for more context.",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 11,
|
||||
"column": 5
|
||||
},
|
||||
"end": {
|
||||
"line": 11,
|
||||
"column": 40
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"prompt": "This is the sweep's source range from the user's main selection of the end cap.",
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 17,
|
||||
"column": 13
|
||||
},
|
||||
"end": {
|
||||
"line": 17,
|
||||
"column": 44
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"kcl_version": "0.2.47"
|
||||
}
|
@ -159,7 +159,6 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
const unconstrainedLocator = page.locator(
|
||||
`[data-constraint-type="${constraintType}"][data-is-constrained="false"]`
|
||||
)
|
||||
await expect(unconstrainedLocator).toBeVisible()
|
||||
await unconstrainedLocator.hover()
|
||||
await expect(
|
||||
await page.getByTestId('constraint-symbol-popover').count()
|
||||
@ -274,8 +273,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
|
||||
let ang = 0
|
||||
|
||||
const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
|
||||
ang = await u.getAngle(`[data-overlay-index="${0}"]`)
|
||||
const line = await u.getBoundingBox('[data-overlay-index="0"]')
|
||||
ang = await u.getAngle('[data-overlay-index="0"]')
|
||||
console.log('line1', line, ang)
|
||||
await clickConstrained({
|
||||
hoverPos: { x: line.x, y: line.y },
|
||||
@ -297,8 +296,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
locator: '[data-overlay-index="0"]',
|
||||
})
|
||||
|
||||
const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`)
|
||||
ang = await u.getAngle(`[data-overlay-index="1"]`)
|
||||
const angledLine = await u.getBoundingBox('[data-overlay-index="1"]')
|
||||
ang = await u.getAngle('[data-overlay-index="1"]')
|
||||
console.log('angledLine1')
|
||||
await clickConstrained({
|
||||
hoverPos: { x: angledLine.x, y: angledLine.y },
|
||||
@ -327,8 +326,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
await page.mouse.move(700, 250)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
let lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`)
|
||||
ang = await u.getAngle(`[data-overlay-index="2"]`)
|
||||
let lineTo = await u.getBoundingBox('[data-overlay-index="2"]')
|
||||
ang = await u.getAngle('[data-overlay-index="2"]')
|
||||
console.log('lineTo1')
|
||||
await clickConstrained({
|
||||
hoverPos: { x: lineTo.x, y: lineTo.y },
|
||||
@ -353,8 +352,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
locator: '[data-overlay-toolbar-index="2"]',
|
||||
})
|
||||
|
||||
const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`)
|
||||
ang = await u.getAngle(`[data-overlay-index="3"]`)
|
||||
const xLineTo = await u.getBoundingBox('[data-overlay-index="3"]')
|
||||
ang = await u.getAngle('[data-overlay-index="3"]')
|
||||
console.log('xlineTo1')
|
||||
await clickConstrained({
|
||||
hoverPos: { x: xLineTo.x, y: xLineTo.y },
|
||||
@ -419,8 +418,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
|
||||
let ang = 0
|
||||
|
||||
const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`)
|
||||
ang = await u.getAngle(`[data-overlay-index="4"]`)
|
||||
const yLineTo = await u.getBoundingBox('[data-overlay-index="4"]')
|
||||
ang = await u.getAngle('[data-overlay-index="4"]')
|
||||
console.log('ylineTo1')
|
||||
await clickUnconstrained({
|
||||
hoverPos: { x: yLineTo.x, y: yLineTo.y - 200 },
|
||||
@ -432,8 +431,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
locator: '[data-overlay-toolbar-index="4"]',
|
||||
})
|
||||
|
||||
const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`)
|
||||
ang = await u.getAngle(`[data-overlay-index="5"]`)
|
||||
const xLine = await u.getBoundingBox('[data-overlay-index="5"]')
|
||||
ang = await u.getAngle('[data-overlay-index="5"]')
|
||||
console.log('xline')
|
||||
await clickUnconstrained({
|
||||
hoverPos: { x: xLine.x, y: xLine.y },
|
||||
@ -501,8 +500,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
|
||||
let ang = 0
|
||||
|
||||
const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`)
|
||||
ang = await u.getAngle(`[data-overlay-index="6"]`)
|
||||
const yLine = await u.getBoundingBox('[data-overlay-index="6"]')
|
||||
ang = await u.getAngle('[data-overlay-index="6"]')
|
||||
console.log('yline1')
|
||||
await clickConstrained({
|
||||
hoverPos: { x: yLine.x, y: yLine.y },
|
||||
@ -515,9 +514,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
const angledLineOfXLength = await u.getBoundingBox(
|
||||
`[data-overlay-index="7"]`
|
||||
'[data-overlay-index="7"]'
|
||||
)
|
||||
ang = await u.getAngle(`[data-overlay-index="7"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="7"]')
|
||||
console.log('angledLineOfXLength1')
|
||||
await clickConstrained({
|
||||
hoverPos: { x: angledLineOfXLength.x, y: angledLineOfXLength.y },
|
||||
@ -547,9 +546,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
const angledLineOfYLength = await u.getBoundingBox(
|
||||
`[data-overlay-index="8"]`
|
||||
'[data-overlay-index="8"]'
|
||||
)
|
||||
ang = await u.getAngle(`[data-overlay-index="8"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="8"]')
|
||||
console.log('angledLineOfYLength1')
|
||||
await clickUnconstrained({
|
||||
hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y },
|
||||
@ -632,8 +631,8 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
|
||||
let ang = 0
|
||||
|
||||
const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`)
|
||||
ang = await u.getAngle(`[data-overlay-index="9"]`)
|
||||
const angledLineToX = await u.getBoundingBox('[data-overlay-index="9"]')
|
||||
ang = await u.getAngle('[data-overlay-index="9"]')
|
||||
console.log('angledLineToX')
|
||||
await clickConstrained({
|
||||
hoverPos: { x: angledLineToX.x, y: angledLineToX.y },
|
||||
@ -659,9 +658,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
const angledLineToY = await u.getBoundingBox(
|
||||
`[data-overlay-index="10"]`
|
||||
'[data-overlay-index="10"]'
|
||||
)
|
||||
ang = await u.getAngle(`[data-overlay-index="10"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="10"]')
|
||||
console.log('angledLineToY')
|
||||
await clickUnconstrained({
|
||||
hoverPos: { x: angledLineToY.x, y: angledLineToY.y },
|
||||
@ -689,9 +688,9 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
const angledLineThatIntersects = await u.getBoundingBox(
|
||||
`[data-overlay-index="11"]`
|
||||
'[data-overlay-index="11"]'
|
||||
)
|
||||
ang = await u.getAngle(`[data-overlay-index="11"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="11"]')
|
||||
console.log('angledLineThatIntersects')
|
||||
await clickUnconstrained({
|
||||
hoverPos: {
|
||||
@ -821,6 +820,138 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
locator: '[data-overlay-toolbar-index="12"]',
|
||||
})
|
||||
})
|
||||
test('for segment [arcTo]', async ({
|
||||
page,
|
||||
editor,
|
||||
homePage,
|
||||
scene,
|
||||
cmdBar,
|
||||
}) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
profile001 = startProfileAt([56.37, 120.33], sketch001)
|
||||
|> line(end = [162.86, 106.48])
|
||||
|> arcTo({
|
||||
interior = [360.16, 231.76],
|
||||
end = [391.48, 131.54]
|
||||
}, %)
|
||||
|> yLine(-131.54, %)
|
||||
|> arc({
|
||||
radius = 126.46,
|
||||
angleStart = 33.53,
|
||||
angleEnd = -141.07
|
||||
}, %)
|
||||
`
|
||||
)
|
||||
localStorage.setItem('disableAxis', 'true')
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// wait for execution done
|
||||
|
||||
await page.getByText('line(end = [162.86, 106.48])').click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(5)
|
||||
|
||||
const clickUnconstrained = _clickUnconstrained(page, editor)
|
||||
const clickConstrained = _clickConstrained(page, editor)
|
||||
|
||||
const arcTo = await u.getBoundingBox('[data-overlay-index="1"]')
|
||||
let ang = await u.getAngle('[data-overlay-index="1"]')
|
||||
console.log('arcTo interior x')
|
||||
await clickUnconstrained({
|
||||
hoverPos: { x: arcTo.x, y: arcTo.y },
|
||||
constraintType: 'xAbsolute',
|
||||
expectBeforeUnconstrained: `arcTo({
|
||||
interior = [360.16, 231.76],
|
||||
end = [391.48, 131.54]
|
||||
}, %)`,
|
||||
expectAfterUnconstrained: `arcTo({
|
||||
interior = [360.16, 231.76],
|
||||
end = [391.48, 131.54]
|
||||
}, %)`,
|
||||
expectFinal: `arcTo({
|
||||
interior = [xAbs001, 231.76],
|
||||
end = [391.48, 131.54]
|
||||
}, %)`,
|
||||
ang: ang,
|
||||
steps: 6,
|
||||
locator: '[data-overlay-toolbar-index="1"]',
|
||||
})
|
||||
|
||||
console.log('arcTo interior y')
|
||||
await clickUnconstrained({
|
||||
hoverPos: { x: arcTo.x, y: arcTo.y },
|
||||
constraintType: 'yAbsolute',
|
||||
expectBeforeUnconstrained: `arcTo({
|
||||
interior = [xAbs001, 231.76],
|
||||
end = [391.48, 131.54]
|
||||
}, %)`,
|
||||
expectAfterUnconstrained: `arcTo({
|
||||
interior = [xAbs001, yAbs001],
|
||||
end = [391.48, 131.54]
|
||||
}, %)`,
|
||||
expectFinal: `arcTo({
|
||||
interior = [xAbs001, 231.76],
|
||||
end = [391.48, 131.54]
|
||||
}, %)`,
|
||||
ang: ang,
|
||||
steps: 10,
|
||||
locator: '[data-overlay-toolbar-index="1"]',
|
||||
})
|
||||
|
||||
console.log('arcTo end x')
|
||||
await clickConstrained({
|
||||
hoverPos: { x: arcTo.x, y: arcTo.y },
|
||||
constraintType: 'xAbsolute',
|
||||
expectBeforeUnconstrained: `arcTo({
|
||||
interior = [xAbs001, 231.76],
|
||||
end = [391.48, 131.54]
|
||||
}, %)`,
|
||||
expectAfterUnconstrained: `arcTo({
|
||||
interior = [xAbs001, 231.76],
|
||||
end = [391.48, 131.54]
|
||||
}, %)`,
|
||||
expectFinal: `arcTo({
|
||||
interior = [xAbs001, 231.76],
|
||||
end = [xAbs002, 131.54]
|
||||
}, %)`,
|
||||
ang: ang + 180,
|
||||
steps: 6,
|
||||
locator: '[data-overlay-toolbar-index="1"]',
|
||||
})
|
||||
|
||||
console.log('arcTo end y')
|
||||
await clickUnconstrained({
|
||||
hoverPos: { x: arcTo.x, y: arcTo.y },
|
||||
constraintType: 'yAbsolute',
|
||||
expectBeforeUnconstrained: `arcTo({
|
||||
interior = [xAbs001, 231.76],
|
||||
end = [xAbs002, 131.54]
|
||||
}, %)`,
|
||||
expectAfterUnconstrained: `arcTo({
|
||||
interior = [xAbs001, 231.76],
|
||||
end = [xAbs002, yAbs002]
|
||||
}, %)`,
|
||||
expectFinal: `arcTo({
|
||||
interior = [xAbs001, 231.76],
|
||||
end = [xAbs002, 131.54]
|
||||
}, %)`,
|
||||
ang: ang + 180,
|
||||
steps: 10,
|
||||
locator: '[data-overlay-toolbar-index="1"]',
|
||||
})
|
||||
})
|
||||
test('for segment [circle]', async ({ page, editor, homePage }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -928,14 +1059,23 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
shouldNormalise: true,
|
||||
})
|
||||
|
||||
await page.locator(`[data-stdlib-fn-name="${stdLibFnName}"]`).click()
|
||||
await page
|
||||
.locator(`[data-stdlib-fn-name="${stdLibFnName}"]`)
|
||||
.first()
|
||||
.click()
|
||||
await page.getByText('Delete Segment').click()
|
||||
|
||||
await editor.expectEditor.not.toContain(codeToBeDeleted, {
|
||||
shouldNormalise: true,
|
||||
})
|
||||
}
|
||||
test('all segment types', async ({ page, editor, homePage }) => {
|
||||
test('all segment types', async ({
|
||||
page,
|
||||
editor,
|
||||
homePage,
|
||||
scene,
|
||||
cmdBar,
|
||||
}) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
@ -958,6 +1098,16 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
offset = 9
|
||||
}, %)
|
||||
|> tangentialArcTo([3.14 + 13, 1.14], %)
|
||||
|> arcTo({
|
||||
interior = [16.25, 5.12],
|
||||
end = [21.61, 4.15]
|
||||
}, %)
|
||||
|> arc({
|
||||
radius = 9.03,
|
||||
angleStart = 40.27,
|
||||
angleEnd = -38.05
|
||||
}, %)
|
||||
|
||||
`
|
||||
)
|
||||
localStorage.setItem('disableAxis', 'true')
|
||||
@ -966,27 +1116,55 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
await u.waitForPageLoad()
|
||||
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await page.getByText('xLine(endAbsolute = 9 - 5)').click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(16)
|
||||
const deleteSegmentSequence = _deleteSegmentSequence(page, editor)
|
||||
|
||||
let segmentToDelete
|
||||
|
||||
const getOverlayByIndex = (index: number) =>
|
||||
u.getBoundingBox(`[data-overlay-index="${index}"]`)
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(14)
|
||||
let ang = await u.getAngle('[data-overlay-index="14"]')
|
||||
|
||||
await editor.scrollToText('angleEnd')
|
||||
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: `arc({
|
||||
radius = 9.03,
|
||||
angleStart = 40.27,
|
||||
angleEnd = -38.05
|
||||
}, %)`,
|
||||
stdLibFnName: 'arc',
|
||||
ang: ang + 180,
|
||||
steps: 6,
|
||||
locator: '[data-overlay-toolbar-index="14"]',
|
||||
})
|
||||
segmentToDelete = await getOverlayByIndex(13)
|
||||
ang = await u.getAngle('[data-overlay-index="13"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: `arcTo({
|
||||
interior = [16.25, 5.12],
|
||||
end = [21.61, 4.15]
|
||||
}, %)`,
|
||||
stdLibFnName: 'arcTo',
|
||||
ang: ang,
|
||||
steps: 6,
|
||||
locator: '[data-overlay-toolbar-index="13"]',
|
||||
})
|
||||
segmentToDelete = await getOverlayByIndex(12)
|
||||
let ang = await u.getAngle(`[data-overlay-index="${12}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="12"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: 'tangentialArcTo([3.14 + 13, 1.14], %)',
|
||||
@ -997,7 +1175,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(11)
|
||||
ang = await u.getAngle(`[data-overlay-index="${11}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="11"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: `angledLineThatIntersects({
|
||||
@ -1012,7 +1190,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(10)
|
||||
ang = await u.getAngle(`[data-overlay-index="${10}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="10"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: 'angledLineToY({ angle = 89, to = 9.14 + 0 }, %)',
|
||||
@ -1022,7 +1200,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(9)
|
||||
ang = await u.getAngle(`[data-overlay-index="${9}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="9"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: 'angledLineToX({ angle = 3 + 0, to = 26 }, %)',
|
||||
@ -1032,7 +1210,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(8)
|
||||
ang = await u.getAngle(`[data-overlay-index="${8}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="8"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted:
|
||||
@ -1043,7 +1221,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(7)
|
||||
ang = await u.getAngle(`[data-overlay-index="${7}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="7"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted:
|
||||
@ -1054,7 +1232,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(6)
|
||||
ang = await u.getAngle(`[data-overlay-index="${6}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="6"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: 'yLine(length = 21.14 + 0)',
|
||||
@ -1064,7 +1242,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(5)
|
||||
ang = await u.getAngle(`[data-overlay-index="${5}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="5"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: 'xLine(length = 26.04)',
|
||||
@ -1074,7 +1252,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(4)
|
||||
ang = await u.getAngle(`[data-overlay-index="${4}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="4"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: 'yLine(endAbsolute = -10.77, tag = $a)',
|
||||
@ -1084,7 +1262,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(3)
|
||||
ang = await u.getAngle(`[data-overlay-index="${3}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="3"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: 'xLine(endAbsolute = 9 - 5)',
|
||||
@ -1094,7 +1272,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(2)
|
||||
ang = await u.getAngle(`[data-overlay-index="${2}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="2"]')
|
||||
await expect(page.getByText('Added variable')).not.toBeVisible()
|
||||
|
||||
const hoverPos = { x: segmentToDelete.x, y: segmentToDelete.y }
|
||||
@ -1127,7 +1305,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(1)
|
||||
ang = await u.getAngle(`[data-overlay-index="${1}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="1"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: 'angledLine({ angle = 3 + 0, length = 32 + 0 }, %)',
|
||||
@ -1137,7 +1315,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
})
|
||||
|
||||
segmentToDelete = await getOverlayByIndex(0)
|
||||
ang = await u.getAngle(`[data-overlay-index="${0}"]`)
|
||||
ang = await u.getAngle('[data-overlay-index="0"]')
|
||||
await deleteSegmentSequence({
|
||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y },
|
||||
codeToBeDeleted: 'line(end = [0.5, -14 + 0])',
|
||||
@ -1366,7 +1544,7 @@ test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
await expect(page.getByText('Added variable')).not.toBeVisible()
|
||||
|
||||
const hoverPos = await u.getBoundingBox(`[data-overlay-index="0"]`)
|
||||
let ang = await u.getAngle(`[data-overlay-index="${0}"]`)
|
||||
let ang = await u.getAngle('[data-overlay-index="0"]')
|
||||
ang += 180
|
||||
|
||||
await page.mouse.move(0, 0)
|
||||
|
@ -832,6 +832,19 @@ pub enum Path {
|
||||
#[ts(type = "[number, number]")]
|
||||
p3: [f64; 2],
|
||||
},
|
||||
ArcThreePoint {
|
||||
#[serde(flatten)]
|
||||
base: BasePath,
|
||||
/// Point 1 of the arc (base on the end of previous segment)
|
||||
#[ts(type = "[number, number]")]
|
||||
p1: [f64; 2],
|
||||
/// Point 2 of the arc (interior kwarg)
|
||||
#[ts(type = "[number, number]")]
|
||||
p2: [f64; 2],
|
||||
/// Point 3 of the arc (end kwarg)
|
||||
#[ts(type = "[number, number]")]
|
||||
p3: [f64; 2],
|
||||
},
|
||||
/// A path that is horizontal.
|
||||
Horizontal {
|
||||
#[serde(flatten)]
|
||||
@ -892,6 +905,7 @@ impl From<&Path> for PathType {
|
||||
Path::AngledLineTo { .. } => Self::AngledLineTo,
|
||||
Path::Base { .. } => Self::Base,
|
||||
Path::Arc { .. } => Self::Arc,
|
||||
Path::ArcThreePoint { .. } => Self::Arc,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -908,6 +922,7 @@ impl Path {
|
||||
Path::Circle { base, .. } => base.geo_meta.id,
|
||||
Path::CircleThreePoint { base, .. } => base.geo_meta.id,
|
||||
Path::Arc { base, .. } => base.geo_meta.id,
|
||||
Path::ArcThreePoint { base, .. } => base.geo_meta.id,
|
||||
}
|
||||
}
|
||||
|
||||
@ -922,6 +937,7 @@ impl Path {
|
||||
Path::Circle { base, .. } => base.tag.clone(),
|
||||
Path::CircleThreePoint { base, .. } => base.tag.clone(),
|
||||
Path::Arc { base, .. } => base.tag.clone(),
|
||||
Path::ArcThreePoint { base, .. } => base.tag.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -936,6 +952,7 @@ impl Path {
|
||||
Path::Circle { base, .. } => base,
|
||||
Path::CircleThreePoint { base, .. } => base,
|
||||
Path::Arc { base, .. } => base,
|
||||
Path::ArcThreePoint { base, .. } => base,
|
||||
}
|
||||
}
|
||||
|
||||
@ -985,6 +1002,10 @@ impl Path {
|
||||
// TODO: Call engine utils to figure this out.
|
||||
linear_distance(self.get_from(), self.get_to())
|
||||
}
|
||||
Self::ArcThreePoint { .. } => {
|
||||
// TODO: Call engine utils to figure this out.
|
||||
linear_distance(self.get_from(), self.get_to())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -999,6 +1020,7 @@ impl Path {
|
||||
Path::Circle { base, .. } => Some(base),
|
||||
Path::CircleThreePoint { base, .. } => Some(base),
|
||||
Path::Arc { base, .. } => Some(base),
|
||||
Path::ArcThreePoint { base, .. } => Some(base),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1010,6 +1032,17 @@ impl Path {
|
||||
center: *center,
|
||||
ccw: *ccw,
|
||||
},
|
||||
Path::ArcThreePoint { p1, p2, p3, .. } => {
|
||||
let circle_center =
|
||||
crate::std::utils::calculate_circle_from_3_points([(*p1).into(), (*p2).into(), (*p3).into()]);
|
||||
let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], p1);
|
||||
let center_point = [circle_center.center.x, circle_center.center.y];
|
||||
GetTangentialInfoFromPathsResult::Circle {
|
||||
center: center_point,
|
||||
ccw: true,
|
||||
radius,
|
||||
}
|
||||
}
|
||||
Path::Circle {
|
||||
center, ccw, radius, ..
|
||||
} => GetTangentialInfoFromPathsResult::Circle {
|
||||
|
@ -252,6 +252,17 @@ pub(crate) async fn do_post_extrude(
|
||||
});
|
||||
Some(extrude_surface)
|
||||
}
|
||||
Path::ArcThreePoint { .. } => {
|
||||
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
|
||||
face_id: *actual_face_id,
|
||||
tag: path.get_base().tag.clone(),
|
||||
geo_meta: GeoMeta {
|
||||
id: path.get_base().geo_meta.id,
|
||||
metadata: path.get_base().geo_meta.metadata,
|
||||
},
|
||||
});
|
||||
Some(extrude_surface)
|
||||
}
|
||||
}
|
||||
} else if no_engine_commands {
|
||||
// Only pre-populate the extrude surface if we are in mock mode.
|
||||
|
@ -23,8 +23,8 @@ use crate::{
|
||||
std::{
|
||||
args::{Args, TyF64},
|
||||
utils::{
|
||||
arc_angles, arc_center_and_end, calculate_circle_center, get_tangential_arc_to_info, get_x_component,
|
||||
get_y_component, intersection_with_parallel_line, TangentialArcInfoInput,
|
||||
arc_angles, arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
|
||||
intersection_with_parallel_line, TangentialArcInfoInput,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -1682,18 +1682,7 @@ pub(crate) async fn inner_arc_to(
|
||||
let interior = data.interior;
|
||||
let end = data.end;
|
||||
|
||||
// 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 ccw = is_ccw(start, interior, end);
|
||||
|
||||
let current_path = Path::Arc {
|
||||
let current_path = Path::ArcThreePoint {
|
||||
base: BasePath {
|
||||
from: from.into(),
|
||||
to: data.end,
|
||||
@ -1704,9 +1693,9 @@ pub(crate) async fn inner_arc_to(
|
||||
metadata: args.source_range.into(),
|
||||
},
|
||||
},
|
||||
center,
|
||||
radius,
|
||||
ccw,
|
||||
p1: start,
|
||||
p2: interior,
|
||||
p3: end,
|
||||
};
|
||||
|
||||
let mut new_sketch = sketch.clone();
|
||||
@ -1719,26 +1708,6 @@ pub(crate) async fn inner_arc_to(
|
||||
Ok(new_sketch)
|
||||
}
|
||||
|
||||
/// Returns true if the three-point arc is counterclockwise. The order of
|
||||
/// parameters is critical.
|
||||
///
|
||||
/// | end
|
||||
/// | /
|
||||
/// | | / interior
|
||||
/// | / /
|
||||
/// | | /
|
||||
/// |/_____________
|
||||
/// start
|
||||
///
|
||||
/// If the slope of the line from start to interior is less than the slope of
|
||||
/// the line from start to end, the arc is counterclockwise.
|
||||
fn is_ccw(start: [f64; 2], interior: [f64; 2], end: [f64; 2]) -> bool {
|
||||
let t1 = (interior[0] - start[0]) * (end[1] - start[1]);
|
||||
let t2 = (end[0] - start[0]) * (interior[1] - start[1]);
|
||||
// If these terms are equal, the points are collinear.
|
||||
t1 > t2
|
||||
}
|
||||
|
||||
/// Data to draw a tangential arc.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
|
||||
#[ts(export)]
|
||||
|
@ -168,7 +168,7 @@ export function Toolbar({
|
||||
|
||||
return (
|
||||
<menu
|
||||
data-currentMode={currentMode}
|
||||
data-current-mode={currentMode}
|
||||
className="max-w-full whitespace-nowrap rounded-b px-2 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 relative border border-chalkboard-30 dark:border-chalkboard-80 border-t-0 shadow-sm"
|
||||
>
|
||||
<ul
|
||||
|
@ -147,7 +147,8 @@ export const ClientSideScene = ({
|
||||
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||
state.matches({ Sketch: 'Rectangle tool' }) ||
|
||||
state.matches({ Sketch: 'Circle tool' }) ||
|
||||
state.matches({ Sketch: 'Circle three point tool' })
|
||||
state.matches({ Sketch: 'Circle three point tool' }) ||
|
||||
state.matches({ Sketch: 'Arc three point tool' })
|
||||
) {
|
||||
cursor = 'crosshair'
|
||||
} else {
|
||||
@ -490,14 +491,19 @@ const SegmentMenu = ({
|
||||
verticalPosition === 'top' ? 'bottom-full' : 'top-full'
|
||||
} z-10 w-36 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50`}
|
||||
>
|
||||
{stdLibFnName !== 'arcTo' && (
|
||||
<button
|
||||
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
|
||||
onClick={() => {
|
||||
send({ type: 'Constrain remove constraints', data: pathToNode })
|
||||
send({
|
||||
type: 'Constrain remove constraints',
|
||||
data: pathToNode,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Remove constraints
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
|
||||
title={
|
||||
|
@ -49,7 +49,19 @@ import {
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT_BODY,
|
||||
TANGENTIAL_ARC_TO__SEGMENT_DASH,
|
||||
ARC_SEGMENT,
|
||||
ARC_SEGMENT_BODY,
|
||||
ARC_SEGMENT_DASH,
|
||||
ARC_ANGLE_END,
|
||||
getParentGroup,
|
||||
ARC_CENTER_TO_FROM,
|
||||
ARC_CENTER_TO_TO,
|
||||
ARC_ANGLE_REFERENCE_LINE,
|
||||
THREE_POINT_ARC_SEGMENT,
|
||||
THREE_POINT_ARC_SEGMENT_BODY,
|
||||
THREE_POINT_ARC_SEGMENT_DASH,
|
||||
THREE_POINT_ARC_HANDLE2,
|
||||
THREE_POINT_ARC_HANDLE3,
|
||||
} from './sceneEntities'
|
||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
import {
|
||||
@ -61,7 +73,7 @@ import {
|
||||
SEGMENT_LENGTH_LABEL_TEXT,
|
||||
} from './sceneInfra'
|
||||
import { Themes, getThemeColorForThreeJs } from 'lib/theme'
|
||||
import { normaliseAngle, roundOff } from 'lib/utils'
|
||||
import { isClockwise, normaliseAngle, roundOff } from 'lib/utils'
|
||||
import {
|
||||
SegmentOverlay,
|
||||
SegmentOverlayPayload,
|
||||
@ -74,6 +86,7 @@ import { Selections } from 'lib/selections'
|
||||
import { calculate_circle_from_3_points } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
const ANGLE_INDICATOR_RADIUS = 30 // in px
|
||||
interface CreateSegmentArgs {
|
||||
input: SegmentInputs
|
||||
prevSegment: Sketch['paths'][number]
|
||||
@ -412,14 +425,28 @@ class TangentialArcToSegment implements SegmentUtils {
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
|
||||
const previousPoint =
|
||||
prevSegment?.type === 'TangentialArcTo'
|
||||
? getTangentPointFromPreviousArc(
|
||||
let previousPoint = prevSegment.from
|
||||
if (prevSegment?.type === 'TangentialArcTo') {
|
||||
previousPoint = getTangentPointFromPreviousArc(
|
||||
prevSegment.center,
|
||||
prevSegment.ccw,
|
||||
prevSegment.to
|
||||
)
|
||||
: prevSegment.from
|
||||
} else if (prevSegment?.type === 'ArcThreePoint') {
|
||||
const arcDetails = calculate_circle_from_3_points(
|
||||
prevSegment.p1[0],
|
||||
prevSegment.p1[1],
|
||||
prevSegment.p2[0],
|
||||
prevSegment.p2[1],
|
||||
prevSegment.p3[0],
|
||||
prevSegment.p3[1]
|
||||
)
|
||||
previousPoint = getTangentPointFromPreviousArc(
|
||||
[arcDetails.center_x, arcDetails.center_y],
|
||||
!isClockwise([prevSegment.p1, prevSegment.p2, prevSegment.p3]),
|
||||
prevSegment.p3
|
||||
)
|
||||
}
|
||||
|
||||
const arcInfo = getTangentialArcToInfo({
|
||||
arcStartPoint: from,
|
||||
@ -591,7 +618,6 @@ class CircleSegment implements SegmentUtils {
|
||||
}
|
||||
const { from, center, radius } = input
|
||||
group.userData.from = from
|
||||
// group.userData.to = to
|
||||
group.userData.center = center
|
||||
group.userData.radius = radius
|
||||
group.userData.prevSegment = prevSegment
|
||||
@ -635,8 +661,7 @@ class CircleSegment implements SegmentUtils {
|
||||
}
|
||||
|
||||
if (radiusLengthIndicator) {
|
||||
// The radius indicator is placed at the midpoint of the radius,
|
||||
// at a 45 degree CCW angle from the positive X-axis
|
||||
// The radius indicator is placed halfway between the center and the start angle point
|
||||
const indicatorPoint = {
|
||||
x: center[0] + (Math.cos(Math.PI / 4) * radius) / 2,
|
||||
y: center[1] + (Math.sin(Math.PI / 4) * radius) / 2,
|
||||
@ -648,6 +673,8 @@ class CircleSegment implements SegmentUtils {
|
||||
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
||||
label.innerText = `${roundOff(radius)}`
|
||||
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
||||
|
||||
// Calculate the angle for the label
|
||||
const isPlaneBackFace = center[0] > indicatorPoint.x
|
||||
label.style.setProperty(
|
||||
'--degree',
|
||||
@ -925,6 +952,585 @@ class CircleThreePointSegment implements SegmentUtils {
|
||||
}
|
||||
}
|
||||
|
||||
class ArcSegment implements SegmentUtils {
|
||||
init: SegmentUtils['init'] = ({
|
||||
prevSegment,
|
||||
input,
|
||||
id,
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
theme,
|
||||
isSelected,
|
||||
sceneInfra,
|
||||
}) => {
|
||||
if (input.type !== 'arc-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { from, to, center, radius, ccw } = input
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
|
||||
|
||||
const group = new Group()
|
||||
const geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw,
|
||||
isDashed: isDraftSegment,
|
||||
scale,
|
||||
})
|
||||
const mat = new MeshBasicMaterial({ color })
|
||||
const arcMesh = new Mesh(geometry, mat)
|
||||
const meshType = isDraftSegment ? ARC_SEGMENT_DASH : ARC_SEGMENT_BODY
|
||||
|
||||
// Create handles for the arc
|
||||
|
||||
const endAngleHandle = createArrowhead(scale, theme, color)
|
||||
endAngleHandle.name = ARC_ANGLE_END
|
||||
endAngleHandle.userData.type = ARC_ANGLE_END
|
||||
|
||||
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
|
||||
|
||||
// A radius indicator that appears from the center to the perimeter
|
||||
const radiusIndicatorGroup = createLengthIndicator({
|
||||
from: center,
|
||||
to: from,
|
||||
scale,
|
||||
})
|
||||
|
||||
const grey = 0xaaaaaa
|
||||
|
||||
// Create a line from the center to the 'to' point
|
||||
const centerToFromLine = createLine({
|
||||
from: center,
|
||||
to: from,
|
||||
scale,
|
||||
color: grey, // Light gray color for the line
|
||||
})
|
||||
centerToFromLine.name = ARC_CENTER_TO_FROM
|
||||
const centerToToLine = createLine({
|
||||
from: center,
|
||||
to,
|
||||
scale,
|
||||
color: grey, // Light gray color for the line
|
||||
})
|
||||
centerToToLine.name = ARC_CENTER_TO_TO
|
||||
const angleReferenceLine = createLine({
|
||||
from: [center[0] + (ANGLE_INDICATOR_RADIUS - 2) * scale, center[1]],
|
||||
to: [center[0] + (ANGLE_INDICATOR_RADIUS + 2) * scale, center[1]],
|
||||
scale,
|
||||
color: grey, // Light gray color for the line
|
||||
})
|
||||
angleReferenceLine.name = ARC_ANGLE_REFERENCE_LINE
|
||||
|
||||
// Create a curved line with an arrow to indicate the angle
|
||||
const angleIndicator = createAngleIndicator({
|
||||
center,
|
||||
radius: ANGLE_INDICATOR_RADIUS, // Half the radius for the indicator
|
||||
startAngle: 0,
|
||||
endAngle,
|
||||
scale,
|
||||
color: grey, // Red color for the angle indicator
|
||||
}) as Line
|
||||
angleIndicator.name = 'angleIndicator'
|
||||
|
||||
// Create a new angle indicator for the end angle
|
||||
const endAngleIndicator = createAngleIndicator({
|
||||
center,
|
||||
radius: ANGLE_INDICATOR_RADIUS, // Half the radius for the indicator
|
||||
startAngle: 0,
|
||||
endAngle: (endAngle * Math.PI) / 180,
|
||||
scale,
|
||||
color: grey, // Green color for the end angle indicator
|
||||
}) as Line
|
||||
endAngleIndicator.name = 'endAngleIndicator'
|
||||
|
||||
// Create a length indicator for the end angle
|
||||
const endAngleLengthIndicator = createLengthIndicator({
|
||||
from: center,
|
||||
to: [
|
||||
center[0] + Math.cos(endAngle) * radius,
|
||||
center[1] + Math.sin(endAngle) * radius,
|
||||
],
|
||||
scale,
|
||||
})
|
||||
endAngleLengthIndicator.name = 'endAngleLengthIndicator'
|
||||
|
||||
arcMesh.userData.type = meshType
|
||||
arcMesh.name = meshType
|
||||
group.userData = {
|
||||
type: ARC_SEGMENT,
|
||||
draft: isDraftSegment,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
radius,
|
||||
center,
|
||||
ccw,
|
||||
prevSegment,
|
||||
pathToNode,
|
||||
isSelected,
|
||||
baseColor,
|
||||
}
|
||||
group.name = ARC_SEGMENT
|
||||
|
||||
group.add(
|
||||
arcMesh,
|
||||
endAngleHandle,
|
||||
circleCenterGroup,
|
||||
radiusIndicatorGroup,
|
||||
centerToFromLine,
|
||||
centerToToLine,
|
||||
angleReferenceLine,
|
||||
angleIndicator,
|
||||
endAngleIndicator,
|
||||
endAngleLengthIndicator
|
||||
)
|
||||
const updateOverlaysCallback = this.update({
|
||||
prevSegment,
|
||||
input,
|
||||
group,
|
||||
scale,
|
||||
sceneInfra,
|
||||
})
|
||||
if (err(updateOverlaysCallback)) return updateOverlaysCallback
|
||||
|
||||
return {
|
||||
group,
|
||||
updateOverlaysCallback,
|
||||
}
|
||||
}
|
||||
|
||||
update: SegmentUtils['update'] = ({
|
||||
prevSegment,
|
||||
input,
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
}) => {
|
||||
if (input.type !== 'arc-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { from, to, center, radius, ccw } = input
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
group.userData.center = center
|
||||
group.userData.radius = radius
|
||||
group.userData.ccw = ccw
|
||||
group.userData.prevSegment = prevSegment
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
|
||||
|
||||
// Normalize the angle to -180 to 180 degrees
|
||||
// const normalizedStartAngle = ((startAngle * 180 / Math.PI) + 180) % 360 - 180
|
||||
const normalizedStartAngle = normaliseAngle((startAngle * 180) / Math.PI)
|
||||
const normalizedEndAngle = (((endAngle * 180) / Math.PI + 180) % 360) - 180
|
||||
|
||||
const endAngleHandle = group.getObjectByName(ARC_ANGLE_END) as Group
|
||||
const radiusLengthIndicator = group.getObjectByName(
|
||||
SEGMENT_LENGTH_LABEL
|
||||
) as Group
|
||||
const circleCenterHandle = group.getObjectByName(
|
||||
CIRCLE_CENTER_HANDLE
|
||||
) as Group
|
||||
|
||||
// Calculate arc length
|
||||
let arcAngle = endAngle - startAngle
|
||||
if (ccw && arcAngle > 0) arcAngle = arcAngle - 2 * Math.PI
|
||||
if (!ccw && arcAngle < 0) arcAngle = arcAngle + 2 * Math.PI
|
||||
|
||||
const arcLength = Math.abs(arcAngle) * radius
|
||||
const pxLength = arcLength / scale
|
||||
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
|
||||
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
|
||||
|
||||
const hoveredParent =
|
||||
sceneInfra.hoveredObject &&
|
||||
getParentGroup(sceneInfra.hoveredObject, [ARC_SEGMENT])
|
||||
let isHandlesVisible = !shouldHideIdle
|
||||
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
|
||||
isHandlesVisible = !shouldHideHover
|
||||
}
|
||||
|
||||
if (endAngleHandle) {
|
||||
endAngleHandle.position.set(to[0], to[1], 0)
|
||||
|
||||
const tangentAngle = endAngle + (Math.PI / 2) * (ccw ? 1 : -1)
|
||||
endAngleHandle.quaternion.setFromUnitVectors(
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(Math.cos(tangentAngle), Math.sin(tangentAngle), 0)
|
||||
)
|
||||
endAngleHandle.scale.set(scale, scale, scale)
|
||||
endAngleHandle.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
if (radiusLengthIndicator) {
|
||||
// The radius indicator is placed halfway between the center and the start angle point
|
||||
const indicatorPoint = {
|
||||
x: center[0] + (Math.cos(startAngle) * radius) / 2,
|
||||
y: center[1] + (Math.sin(startAngle) * radius) / 2,
|
||||
}
|
||||
const labelWrapper = radiusLengthIndicator.getObjectByName(
|
||||
SEGMENT_LENGTH_LABEL_TEXT
|
||||
) as CSS2DObject
|
||||
const labelWrapperElem = labelWrapper.element as HTMLDivElement
|
||||
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
||||
label.innerText = `R:${roundOff(radius)}${'\n'}A:${roundOff(
|
||||
roundOff((startAngle * 180) / Math.PI)
|
||||
)}`
|
||||
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
||||
|
||||
// Calculate the angle for the label
|
||||
label.style.setProperty('--degree', `-${startAngle}rad`)
|
||||
label.style.setProperty('--x', `0px`)
|
||||
label.style.setProperty('--y', `0px`)
|
||||
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
|
||||
radiusLengthIndicator.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
if (circleCenterHandle) {
|
||||
circleCenterHandle.position.set(center[0], center[1], 0)
|
||||
circleCenterHandle.scale.set(scale, scale, scale)
|
||||
circleCenterHandle.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const arcSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === ARC_SEGMENT_BODY
|
||||
) as Mesh
|
||||
|
||||
if (arcSegmentBody) {
|
||||
const newGeo = createArcGeometry({
|
||||
radius,
|
||||
center,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw,
|
||||
scale,
|
||||
})
|
||||
arcSegmentBody.geometry = newGeo
|
||||
}
|
||||
|
||||
const arcSegmentBodyDashed = group.getObjectByName(ARC_SEGMENT_DASH)
|
||||
if (arcSegmentBodyDashed instanceof Mesh) {
|
||||
arcSegmentBodyDashed.geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw,
|
||||
isDashed: true,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
|
||||
const centerToFromLine = group.getObjectByName(ARC_CENTER_TO_FROM) as Line
|
||||
if (centerToFromLine) {
|
||||
updateLine(centerToFromLine, { from: center, to: from, scale })
|
||||
centerToFromLine.visible = isHandlesVisible
|
||||
}
|
||||
const centerToToLine = group.getObjectByName(ARC_CENTER_TO_TO) as Line
|
||||
if (centerToToLine) {
|
||||
updateLine(centerToToLine, { from: center, to, scale })
|
||||
centerToToLine.visible = isHandlesVisible
|
||||
}
|
||||
const angleReferenceLine = group.getObjectByName(
|
||||
ARC_ANGLE_REFERENCE_LINE
|
||||
) as Line
|
||||
if (angleReferenceLine) {
|
||||
updateLine(angleReferenceLine, {
|
||||
from: center,
|
||||
to: [center[0] + 34 * scale, center[1]],
|
||||
scale,
|
||||
})
|
||||
angleReferenceLine.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const angleIndicator = group.getObjectByName('angleIndicator') as Line
|
||||
if (angleIndicator) {
|
||||
updateAngleIndicator(angleIndicator, {
|
||||
center,
|
||||
radiusPx: ANGLE_INDICATOR_RADIUS - 10,
|
||||
startAngle: 0,
|
||||
endAngle: (normalizedStartAngle * Math.PI) / 180,
|
||||
scale,
|
||||
})
|
||||
angleIndicator.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const endAngleIndicator = group.getObjectByName('endAngleIndicator') as Line
|
||||
if (endAngleIndicator) {
|
||||
updateAngleIndicator(endAngleIndicator, {
|
||||
center,
|
||||
radiusPx: ANGLE_INDICATOR_RADIUS,
|
||||
startAngle: 0,
|
||||
endAngle: (normalizedEndAngle * Math.PI) / 180,
|
||||
scale,
|
||||
})
|
||||
endAngleIndicator.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const endAngleLengthIndicator = group.getObjectByName(
|
||||
'endAngleLengthIndicator'
|
||||
) as Group
|
||||
if (endAngleLengthIndicator) {
|
||||
const labelWrapper = endAngleLengthIndicator.getObjectByName(
|
||||
SEGMENT_LENGTH_LABEL_TEXT
|
||||
) as CSS2DObject
|
||||
const labelWrapperElem = labelWrapper.element as HTMLDivElement
|
||||
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
||||
label.innerText = `A:${roundOff(normalizedEndAngle)}`
|
||||
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
||||
|
||||
// Position the label
|
||||
const indicatorPoint = {
|
||||
x: center[0] + (Math.cos(endAngle) * radius) / 2,
|
||||
y: center[1] + (Math.sin(endAngle) * radius) / 2,
|
||||
}
|
||||
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
|
||||
endAngleLengthIndicator.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
return () =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
handle: endAngleHandle,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from,
|
||||
to,
|
||||
angle: endAngle + (Math.PI / 2) * (ccw ? 1 : -1),
|
||||
hasThreeDotMenu: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class ThreePointArcSegment implements SegmentUtils {
|
||||
init: SegmentUtils['init'] = ({
|
||||
input,
|
||||
id,
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
theme,
|
||||
isSelected = false,
|
||||
sceneInfra,
|
||||
prevSegment,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { p1, p2, p3 } = input
|
||||
const { center_x, center_y, radius } = calculate_circle_from_3_points(
|
||||
p1[0],
|
||||
p1[1],
|
||||
p2[0],
|
||||
p2[1],
|
||||
p3[0],
|
||||
p3[1]
|
||||
)
|
||||
const center: [number, number] = [center_x, center_y]
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
||||
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
|
||||
|
||||
const group = new Group()
|
||||
const geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw: !isClockwise([p1, p2, p3]),
|
||||
isDashed: isDraftSegment,
|
||||
scale,
|
||||
})
|
||||
const mat = new MeshBasicMaterial({ color })
|
||||
const arcMesh = new Mesh(geometry, mat)
|
||||
const meshType = isDraftSegment
|
||||
? THREE_POINT_ARC_SEGMENT_DASH
|
||||
: THREE_POINT_ARC_SEGMENT_BODY
|
||||
|
||||
// Create handles for p2 and p3 using createCircleThreePointHandle
|
||||
const p2Handle = createCircleThreePointHandle(
|
||||
scale,
|
||||
theme,
|
||||
THREE_POINT_ARC_HANDLE2,
|
||||
color
|
||||
)
|
||||
p2Handle.position.set(p2[0], p2[1], 0)
|
||||
|
||||
const p3Handle = createCircleThreePointHandle(
|
||||
scale,
|
||||
theme,
|
||||
THREE_POINT_ARC_HANDLE3,
|
||||
color
|
||||
)
|
||||
p3Handle.position.set(p3[0], p3[1], 0)
|
||||
|
||||
arcMesh.userData.type = meshType
|
||||
arcMesh.name = meshType
|
||||
group.userData = {
|
||||
type: THREE_POINT_ARC_SEGMENT,
|
||||
draft: isDraftSegment,
|
||||
id,
|
||||
from: p1,
|
||||
to: p3,
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
radius,
|
||||
center,
|
||||
ccw: false,
|
||||
prevSegment,
|
||||
pathToNode,
|
||||
isSelected,
|
||||
baseColor,
|
||||
}
|
||||
group.name = THREE_POINT_ARC_SEGMENT
|
||||
|
||||
group.add(arcMesh, p2Handle, p3Handle)
|
||||
const updateOverlaysCallback = this.update({
|
||||
prevSegment,
|
||||
input,
|
||||
group,
|
||||
scale,
|
||||
sceneInfra,
|
||||
})
|
||||
if (err(updateOverlaysCallback)) return updateOverlaysCallback
|
||||
|
||||
return {
|
||||
group,
|
||||
updateOverlaysCallback,
|
||||
}
|
||||
}
|
||||
|
||||
update: SegmentUtils['update'] = ({
|
||||
prevSegment,
|
||||
input,
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { p1, p2, p3 } = input
|
||||
const { center_x, center_y, radius } = calculate_circle_from_3_points(
|
||||
p1[0],
|
||||
p1[1],
|
||||
p2[0],
|
||||
p2[1],
|
||||
p3[0],
|
||||
p3[1]
|
||||
)
|
||||
const center: [number, number] = [center_x, center_y]
|
||||
group.userData.from = p1
|
||||
group.userData.to = p3
|
||||
group.userData.p1 = p1
|
||||
group.userData.p2 = p2
|
||||
group.userData.p3 = p3
|
||||
group.userData.center = center
|
||||
group.userData.radius = radius
|
||||
group.userData.prevSegment = prevSegment
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
||||
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
|
||||
|
||||
const p2Handle = group.getObjectByName(THREE_POINT_ARC_HANDLE2) as Group
|
||||
const p3Handle = group.getObjectByName(THREE_POINT_ARC_HANDLE3) as Group
|
||||
|
||||
const arcSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === THREE_POINT_ARC_SEGMENT_BODY
|
||||
) as Mesh
|
||||
|
||||
if (arcSegmentBody) {
|
||||
const newGeo = createArcGeometry({
|
||||
radius,
|
||||
center,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw: !isClockwise([p1, p2, p3]),
|
||||
scale,
|
||||
})
|
||||
arcSegmentBody.geometry = newGeo
|
||||
}
|
||||
|
||||
const arcSegmentBodyDashed = group.getObjectByName(
|
||||
THREE_POINT_ARC_SEGMENT_DASH
|
||||
)
|
||||
if (arcSegmentBodyDashed instanceof Mesh) {
|
||||
arcSegmentBodyDashed.geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw: !isClockwise([p1, p2, p3]),
|
||||
isDashed: true,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
|
||||
if (p2Handle) {
|
||||
p2Handle.position.set(p2[0], p2[1], 0)
|
||||
p2Handle.scale.set(scale, scale, scale)
|
||||
p2Handle.visible = true
|
||||
}
|
||||
|
||||
if (p3Handle) {
|
||||
p3Handle.position.set(p3[0], p3[1], 0)
|
||||
p3Handle.scale.set(scale, scale, scale)
|
||||
p3Handle.visible = true
|
||||
}
|
||||
|
||||
return () => {
|
||||
const overlays: SegmentOverlays = {}
|
||||
const overlayDetails = [p2Handle, p3Handle].map((handle, index) =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
handle: handle,
|
||||
group,
|
||||
isHandlesVisible: true,
|
||||
from: p1,
|
||||
to: p3,
|
||||
angle: endAngle + Math.PI / 2,
|
||||
hasThreeDotMenu: true,
|
||||
})
|
||||
)
|
||||
const segmentOverlays: SegmentOverlay[] = []
|
||||
|
||||
overlayDetails.forEach((payload, index) => {
|
||||
if (payload?.type === 'set-one') {
|
||||
overlays[payload.pathToNodeString] = payload.seg
|
||||
// Add filterValue: 'interior' for p2 and 'end' for p3
|
||||
segmentOverlays.push({
|
||||
...payload.seg[0],
|
||||
filterValue: index === 0 ? 'interior' : 'end',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const segmentOverlayPayload: SegmentOverlayPayload = {
|
||||
type: 'set-one',
|
||||
pathToNodeString:
|
||||
overlayDetails[0]?.type === 'set-one'
|
||||
? overlayDetails[0].pathToNodeString
|
||||
: '',
|
||||
seg: segmentOverlays,
|
||||
}
|
||||
return segmentOverlayPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createProfileStartHandle({
|
||||
from,
|
||||
isDraft = false,
|
||||
@ -1010,7 +1616,7 @@ function createCircleCenterHandle(
|
||||
function createCircleThreePointHandle(
|
||||
scale = 1,
|
||||
theme: Themes,
|
||||
name: `circle-three-point-handle${'1' | '2' | '3'}`,
|
||||
name: string,
|
||||
color?: number
|
||||
): Group {
|
||||
const circleCenterGroup = new Group()
|
||||
@ -1249,9 +1855,11 @@ export function createArcGeometry({
|
||||
)
|
||||
)
|
||||
const remainingArcGeometry = new ExtrudeGeometry(shape, {
|
||||
steps: 50,
|
||||
steps: 1,
|
||||
bevelEnabled: false,
|
||||
extrudePath: remainingArcPath,
|
||||
extrudePath: new CatmullRomCurve3(
|
||||
remainingArcPoints.map((p) => new Vector3(p.x, p.y, 0))
|
||||
),
|
||||
})
|
||||
dashGeometries.push(remainingArcGeometry)
|
||||
}
|
||||
@ -1351,10 +1959,111 @@ export function dashedStraight(
|
||||
geo.userData.type = 'dashed'
|
||||
return geo
|
||||
}
|
||||
function createLine({
|
||||
from,
|
||||
to,
|
||||
scale,
|
||||
color,
|
||||
}: {
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
scale: number
|
||||
color: number
|
||||
}): Line {
|
||||
// Implementation for creating a line
|
||||
const lineGeometry = new BufferGeometry().setFromPoints([
|
||||
new Vector3(from[0], from[1], 0),
|
||||
new Vector3(to[0], to[1], 0),
|
||||
])
|
||||
const lineMaterial = new LineBasicMaterial({ color })
|
||||
return new Line(lineGeometry, lineMaterial)
|
||||
}
|
||||
|
||||
function updateLine(
|
||||
line: Line,
|
||||
{
|
||||
from,
|
||||
to,
|
||||
scale,
|
||||
}: { from: [number, number]; to: [number, number]; scale: number }
|
||||
) {
|
||||
// Implementation for updating a line
|
||||
const points = [
|
||||
new Vector3(from[0], from[1], 0),
|
||||
new Vector3(to[0], to[1], 0),
|
||||
]
|
||||
line.geometry.setFromPoints(points)
|
||||
}
|
||||
|
||||
function createAngleIndicator({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
scale,
|
||||
color,
|
||||
}: {
|
||||
center: [number, number]
|
||||
radius: number
|
||||
startAngle: number
|
||||
endAngle: number
|
||||
scale: number
|
||||
color: number
|
||||
}): Line {
|
||||
// Implementation for creating an angle indicator
|
||||
const curve = new EllipseCurve(
|
||||
center[0],
|
||||
center[1],
|
||||
radius,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
false,
|
||||
0
|
||||
)
|
||||
const points = curve.getPoints(50)
|
||||
const geometry = new BufferGeometry().setFromPoints(points)
|
||||
const material = new LineBasicMaterial({ color })
|
||||
return new Line(geometry, material)
|
||||
}
|
||||
|
||||
function updateAngleIndicator(
|
||||
angleIndicator: Line,
|
||||
{
|
||||
center,
|
||||
radiusPx,
|
||||
startAngle,
|
||||
endAngle,
|
||||
scale,
|
||||
}: {
|
||||
center: [number, number]
|
||||
radiusPx: number
|
||||
startAngle: number
|
||||
endAngle: number
|
||||
scale: number
|
||||
}
|
||||
) {
|
||||
// Implementation for updating an angle indicator
|
||||
|
||||
const curve = new EllipseCurve(
|
||||
center[0],
|
||||
center[1],
|
||||
radiusPx * scale,
|
||||
radiusPx * scale,
|
||||
startAngle,
|
||||
endAngle,
|
||||
endAngle < startAngle,
|
||||
0
|
||||
)
|
||||
const points = curve.getPoints(50)
|
||||
angleIndicator.geometry.setFromPoints(points)
|
||||
}
|
||||
|
||||
export const segmentUtils = {
|
||||
straight: new StraightSegment(),
|
||||
tangentialArcTo: new TangentialArcToSegment(),
|
||||
circle: new CircleSegment(),
|
||||
circleThreePoint: new CircleThreePointSegment(),
|
||||
arc: new ArcSegment(),
|
||||
threePointArc: new ThreePointArcSegment(),
|
||||
} as const
|
||||
|
@ -70,6 +70,7 @@ import {
|
||||
import {
|
||||
KclValue,
|
||||
PathToNode,
|
||||
PipeExpression,
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
parse,
|
||||
@ -1497,6 +1498,48 @@ export const ModelingMachineProvider = ({
|
||||
return result
|
||||
}
|
||||
),
|
||||
'set-up-draft-arc-three-point': fromPromise(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
return reject('No sketch details or data')
|
||||
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
const result = await sceneEntitiesManager.setupDraftArcThreePoint(
|
||||
sketchDetails.sketchEntryNodePath,
|
||||
sketchDetails.sketchNodePaths,
|
||||
sketchDetails.planeNodePath,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin,
|
||||
data
|
||||
)
|
||||
if (err(result)) return reject(result)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||
|
||||
return result
|
||||
}
|
||||
),
|
||||
'set-up-draft-arc': fromPromise(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
return reject('No sketch details or data')
|
||||
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
const result = await sceneEntitiesManager.setupDraftArc(
|
||||
sketchDetails.sketchEntryNodePath,
|
||||
sketchDetails.sketchNodePaths,
|
||||
sketchDetails.planeNodePath,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin,
|
||||
data
|
||||
)
|
||||
if (err(result)) return reject(result)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||
|
||||
return result
|
||||
}
|
||||
),
|
||||
'setup-client-side-sketch-segments': fromPromise(
|
||||
async ({ input: { sketchDetails, selectionRanges } }) => {
|
||||
if (!sketchDetails) return
|
||||
@ -1568,10 +1611,34 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
|
||||
const indexToDelete = sketchDetails?.expressionIndexToDelete || -1
|
||||
let isLastInPipeThreePointArc = false
|
||||
if (indexToDelete >= 0) {
|
||||
// this is the expression that was added when as sketch tool was used but not completed
|
||||
// i.e first click for the center of the circle, but not the second click for the radius
|
||||
// we added a circle to editor, but they bailed out early so we should remove it
|
||||
|
||||
const pipe = getNodeFromPath<PipeExpression>(
|
||||
moddedAst,
|
||||
pathToProfile,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipe)) {
|
||||
isLastInPipeThreePointArc = false
|
||||
} else {
|
||||
const lastInPipe = pipe?.node?.body?.[pipe.node.body.length - 1]
|
||||
if (
|
||||
lastInPipe &&
|
||||
Number(pathToProfile[1][0]) === indexToDelete &&
|
||||
lastInPipe.type === 'CallExpression' &&
|
||||
lastInPipe.callee.type === 'Identifier' &&
|
||||
lastInPipe.callee.name === 'arcTo'
|
||||
) {
|
||||
isLastInPipeThreePointArc = true
|
||||
pipe.node.body = pipe.node.body.slice(0, -1)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLastInPipeThreePointArc) {
|
||||
moddedAst.body.splice(indexToDelete, 1)
|
||||
// make sure the deleted expression is removed from the sketchNodePaths
|
||||
updatedSketchNodePaths = updatedSketchNodePaths.filter(
|
||||
@ -1584,8 +1651,13 @@ export const ModelingMachineProvider = ({
|
||||
? pathToProfile
|
||||
: updatedSketchNodePaths[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (doesNeedSplitting || indexToDelete >= 0) {
|
||||
if (
|
||||
doesNeedSplitting ||
|
||||
indexToDelete >= 0 ||
|
||||
isLastInPipeThreePointArc
|
||||
) {
|
||||
await kclManager.executeAstMock(moddedAst)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(moddedAst)
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ export type ToolTip =
|
||||
| 'tangentialArcTo'
|
||||
| 'circle'
|
||||
| 'circleThreePoint'
|
||||
| 'arcTo'
|
||||
| 'arc'
|
||||
|
||||
export const toolTips: Array<ToolTip> = [
|
||||
'line',
|
||||
@ -44,6 +46,8 @@ export const toolTips: Array<ToolTip> = [
|
||||
'angledLineThatIntersects',
|
||||
'tangentialArcTo',
|
||||
'circleThreePoint',
|
||||
'arc',
|
||||
'arcTo',
|
||||
]
|
||||
|
||||
export async function executeAst({
|
||||
|
@ -48,6 +48,7 @@ import {
|
||||
RawArgs,
|
||||
CreatedSketchExprResult,
|
||||
SketchLineHelperKw,
|
||||
InputArgKeys,
|
||||
} from 'lang/std/stdTypes'
|
||||
|
||||
import {
|
||||
@ -984,7 +985,10 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode,
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
['body', 'PipeExpression'],
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
@ -1227,6 +1231,658 @@ export const circle: SketchLineHelperKw = {
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
export const arc: SketchLineHelper = {
|
||||
add: ({
|
||||
node,
|
||||
variables,
|
||||
pathToNode,
|
||||
segmentInput,
|
||||
replaceExistingCallback,
|
||||
spliceBetween,
|
||||
}) => {
|
||||
if (segmentInput.type !== 'arc-segment') return ARC_SEGMENT_ERR
|
||||
const { center, radius, from, to } = segmentInput
|
||||
const _node = { ...node }
|
||||
|
||||
const nodeMeta = getNodeFromPath<PipeExpression | CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: pipe } = nodeMeta
|
||||
|
||||
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
const { node: varDec } = nodeMeta2
|
||||
|
||||
// Calculate start angle (from center to 'from' point)
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
|
||||
// Calculate end angle (from center to 'to' point)
|
||||
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
|
||||
|
||||
// Create literals for the angles (convert to degrees)
|
||||
const startAngleDegrees = (startAngle * 180) / Math.PI
|
||||
const endAngleDegrees = (endAngle * 180) / Math.PI
|
||||
|
||||
// Create the arc call expression
|
||||
const arcObj = createObjectExpression({
|
||||
radius: createLiteral(roundOff(radius)),
|
||||
angleStart: createLiteral(roundOff(startAngleDegrees)),
|
||||
angleEnd: createLiteral(roundOff(endAngleDegrees)),
|
||||
})
|
||||
|
||||
const newArc = createCallExpression('arc', [
|
||||
arcObj,
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
|
||||
if (
|
||||
spliceBetween &&
|
||||
!replaceExistingCallback &&
|
||||
pipe.type === 'PipeExpression'
|
||||
) {
|
||||
const pathToNodeIndex = pathToNode.findIndex(
|
||||
(x) => x[1] === 'PipeExpression'
|
||||
)
|
||||
const pipeIndex = pathToNode[pathToNodeIndex + 1][0]
|
||||
if (typeof pipeIndex === 'undefined' || typeof pipeIndex === 'string') {
|
||||
return new Error('pipeIndex is undefined')
|
||||
}
|
||||
pipe.body = [
|
||||
...pipe.body.slice(0, pipeIndex),
|
||||
newArc,
|
||||
...pipe.body.slice(pipeIndex),
|
||||
]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
if (replaceExistingCallback && pipe.type !== 'CallExpression') {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const result = replaceExistingCallback([
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'center',
|
||||
argType: 'xRelative',
|
||||
expr: createLiteral(roundOff(center[0])),
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'center',
|
||||
argType: 'yRelative',
|
||||
expr: createLiteral(roundOff(center[1])),
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'radius',
|
||||
argType: 'radius',
|
||||
expr: createLiteral(roundOff(radius)),
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'angle',
|
||||
argType: 'angle',
|
||||
expr: createLiteral(roundOff(startAngleDegrees)),
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'angle',
|
||||
argType: 'angle',
|
||||
expr: createLiteral(roundOff(endAngleDegrees)),
|
||||
},
|
||||
])
|
||||
if (err(result)) return result
|
||||
const { callExp, valueUsedInTransform } = result
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
valueUsedInTransform,
|
||||
}
|
||||
}
|
||||
|
||||
if (pipe.type === 'PipeExpression') {
|
||||
pipe.body = [...pipe.body, newArc]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
}
|
||||
} else {
|
||||
varDec.init = createPipeExpression([varDec.init, newArc])
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, input }) => {
|
||||
if (input.type !== 'arc-segment') return ARC_SEGMENT_ERR
|
||||
const { center, radius, from, to } = input
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: callExpression, shallowPath } = nodeMeta
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
|
||||
if (firstArg.type !== 'ObjectExpression') {
|
||||
return new Error('Expected object expression as first argument')
|
||||
}
|
||||
|
||||
// Calculate start angle (from center to 'from' point)
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
|
||||
// Calculate end angle (from center to 'to' point)
|
||||
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
|
||||
|
||||
// Create literals for the angles (convert to degrees)
|
||||
const startAngleDegrees = (startAngle * 180) / Math.PI
|
||||
const endAngleDegrees = (endAngle * 180) / Math.PI
|
||||
|
||||
// Update radius
|
||||
const newRadius = createLiteral(roundOff(radius))
|
||||
mutateObjExpProp(firstArg, newRadius, 'radius')
|
||||
|
||||
// Update angleStart
|
||||
const newAngleStart = createLiteral(roundOff(startAngleDegrees))
|
||||
mutateObjExpProp(firstArg, newAngleStart, 'angleStart')
|
||||
|
||||
// Update angleEnd
|
||||
const newAngleEnd = createLiteral(roundOff(endAngleDegrees))
|
||||
mutateObjExpProp(firstArg, newAngleEnd, 'angleEnd')
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: shallowPath,
|
||||
}
|
||||
},
|
||||
getTag: getTag(),
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, code, pathToNode, filterValue) => {
|
||||
// TODO this isn't quiet right, the filter value needs to be added to group the radius and start angle together
|
||||
// with the end angle by itself,
|
||||
// also both angles are just called angle, which is not correct
|
||||
if (callExp.type !== 'CallExpression') return []
|
||||
const args = callExp.arguments
|
||||
if (args.length < 1) return []
|
||||
|
||||
const firstArg = args[0]
|
||||
if (firstArg.type !== 'ObjectExpression') return []
|
||||
|
||||
// Find radius, angleStart, and angleEnd properties
|
||||
const radiusProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'radius'
|
||||
)
|
||||
|
||||
const angleStartProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'angleStart'
|
||||
)
|
||||
|
||||
const angleEndProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'angleEnd'
|
||||
)
|
||||
|
||||
if (!radiusProp || !angleStartProp || !angleEndProp) return []
|
||||
|
||||
const pathToFirstArg: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
const pathToRadiusProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(radiusProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToAngleStartProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(angleStartProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToAngleEndProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(angleEndProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToRadiusValue: PathToNode = [
|
||||
...pathToRadiusProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const pathToAngleStartValue: PathToNode = [
|
||||
...pathToAngleStartProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const pathToAngleEndValue: PathToNode = [
|
||||
...pathToAngleEndProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const constraints: ConstrainInfo[] = [
|
||||
constrainInfo(
|
||||
'radius',
|
||||
isNotLiteralArrayOrStatic(radiusProp.value),
|
||||
code.slice(radiusProp.value.start, radiusProp.value.end),
|
||||
'arc',
|
||||
'radius',
|
||||
topLevelRange(radiusProp.value.start, radiusProp.value.end),
|
||||
pathToRadiusValue
|
||||
),
|
||||
constrainInfo(
|
||||
'angle',
|
||||
isNotLiteralArrayOrStatic(angleStartProp.value),
|
||||
code.slice(angleStartProp.value.start, angleStartProp.value.end),
|
||||
'arc',
|
||||
'angleStart',
|
||||
topLevelRange(angleStartProp.value.start, angleStartProp.value.end),
|
||||
pathToAngleStartValue
|
||||
),
|
||||
constrainInfo(
|
||||
'angle',
|
||||
isNotLiteralArrayOrStatic(angleEndProp.value),
|
||||
code.slice(angleEndProp.value.start, angleEndProp.value.end),
|
||||
'arc',
|
||||
'angleEnd',
|
||||
topLevelRange(angleEndProp.value.start, angleEndProp.value.end),
|
||||
pathToAngleEndValue
|
||||
),
|
||||
]
|
||||
|
||||
return constraints
|
||||
},
|
||||
}
|
||||
export const arcTo: SketchLineHelper = {
|
||||
add: ({
|
||||
node,
|
||||
variables,
|
||||
pathToNode,
|
||||
segmentInput,
|
||||
replaceExistingCallback,
|
||||
spliceBetween,
|
||||
}) => {
|
||||
if (segmentInput.type !== 'circle-three-point-segment')
|
||||
return ARC_SEGMENT_ERR
|
||||
|
||||
const { p2, p3 } = segmentInput
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: pipe } = nodeMeta
|
||||
|
||||
// p1 is the start point (from the previous segment)
|
||||
// p2 is the interior point
|
||||
// p3 is the end point
|
||||
const interior = createArrayExpression([
|
||||
createLiteral(roundOff(p2[0], 2)),
|
||||
createLiteral(roundOff(p2[1], 2)),
|
||||
])
|
||||
|
||||
const end = createArrayExpression([
|
||||
createLiteral(roundOff(p3[0], 2)),
|
||||
createLiteral(roundOff(p3[1], 2)),
|
||||
])
|
||||
|
||||
if (replaceExistingCallback) {
|
||||
const result = replaceExistingCallback([
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'interior' as InputArgKeys,
|
||||
argType: 'xAbsolute',
|
||||
expr: createLiteral(0) as any, // This is a workaround, the actual value will be set later
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'end' as InputArgKeys,
|
||||
argType: 'yAbsolute',
|
||||
expr: createLiteral(0) as any, // This is a workaround, the actual value will be set later
|
||||
},
|
||||
])
|
||||
if (err(result)) return result
|
||||
const { callExp, valueUsedInTransform } = result
|
||||
|
||||
// Now manually update the object properties
|
||||
if (
|
||||
callExp.type === 'CallExpression' &&
|
||||
callExp.arguments[0]?.type === 'ObjectExpression'
|
||||
) {
|
||||
const objExp = callExp.arguments[0]
|
||||
const interiorProp = objExp.properties.find(
|
||||
(p) =>
|
||||
p.type === 'ObjectProperty' &&
|
||||
p.key.type === 'Identifier' &&
|
||||
p.key.name === 'interior'
|
||||
)
|
||||
const endProp = objExp.properties.find(
|
||||
(p) =>
|
||||
p.type === 'ObjectProperty' &&
|
||||
p.key.type === 'Identifier' &&
|
||||
p.key.name === 'end'
|
||||
)
|
||||
|
||||
if (interiorProp && interiorProp.type === 'ObjectProperty') {
|
||||
interiorProp.value = interior
|
||||
}
|
||||
if (endProp && endProp.type === 'ObjectProperty') {
|
||||
endProp.value = end
|
||||
}
|
||||
}
|
||||
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
pipe.body[callIndex] = callExp
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
valueUsedInTransform,
|
||||
}
|
||||
}
|
||||
|
||||
const objExp = createObjectExpression({
|
||||
interior,
|
||||
end,
|
||||
})
|
||||
|
||||
const newLine = createCallExpression('arcTo', [
|
||||
objExp,
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
|
||||
if (spliceBetween) {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
pipe.body.splice(callIndex + 1, 0, newLine)
|
||||
} else if (pipe.type === 'PipeExpression') {
|
||||
pipe.body.push(newLine)
|
||||
} else {
|
||||
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
const { node: varDec } = nodeMeta2
|
||||
varDec.init = createPipeExpression([varDec.init, newLine])
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([key, _]) => key === 'init') + 1
|
||||
),
|
||||
['body', 'PipeExpression'],
|
||||
[1, 'CallExpression'],
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
}
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, input }) => {
|
||||
if (input.type !== 'circle-three-point-segment') return ARC_SEGMENT_ERR
|
||||
|
||||
const { p1, p2, p3 } = input
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
// Update the first argument which should be an object with interior and end properties
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
if (!firstArg) return new Error('Missing first argument in arcTo')
|
||||
|
||||
const interiorPoint = createArrayExpression([
|
||||
createLiteral(roundOff(p2[0], 2)),
|
||||
createLiteral(roundOff(p2[1], 2)),
|
||||
])
|
||||
|
||||
const endPoint = createArrayExpression([
|
||||
createLiteral(roundOff(p3[0], 2)),
|
||||
createLiteral(roundOff(p3[1], 2)),
|
||||
])
|
||||
|
||||
mutateObjExpProp(firstArg, interiorPoint, 'interior')
|
||||
mutateObjExpProp(firstArg, endPoint, 'end')
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
},
|
||||
getTag: getTag(),
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, code, pathToNode, filterValue) => {
|
||||
if (callExp.type !== 'CallExpression') return []
|
||||
const args = callExp.arguments
|
||||
if (args.length < 1) return []
|
||||
|
||||
const firstArg = args[0]
|
||||
if (firstArg.type !== 'ObjectExpression') return []
|
||||
|
||||
// Find interior and end properties
|
||||
const interiorProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'interior'
|
||||
)
|
||||
|
||||
const endProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'end'
|
||||
)
|
||||
|
||||
if (!interiorProp || !endProp) return []
|
||||
if (
|
||||
interiorProp.value.type !== 'ArrayExpression' ||
|
||||
endProp.value.type !== 'ArrayExpression'
|
||||
)
|
||||
return []
|
||||
|
||||
const interiorArr = interiorProp.value
|
||||
const endArr = endProp.value
|
||||
|
||||
if (interiorArr.elements.length < 2 || endArr.elements.length < 2) return []
|
||||
|
||||
const pathToFirstArg: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
const pathToInteriorProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(interiorProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToEndProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(endProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToInteriorValue: PathToNode = [
|
||||
...pathToInteriorProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const pathToEndValue: PathToNode = [
|
||||
...pathToEndProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const pathToInteriorX: PathToNode = [
|
||||
...pathToInteriorValue,
|
||||
['elements', 'ArrayExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
const pathToInteriorY: PathToNode = [
|
||||
...pathToInteriorValue,
|
||||
['elements', 'ArrayExpression'],
|
||||
[1, 'index'],
|
||||
]
|
||||
|
||||
const pathToEndX: PathToNode = [
|
||||
...pathToEndValue,
|
||||
['elements', 'ArrayExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
const pathToEndY: PathToNode = [
|
||||
...pathToEndValue,
|
||||
['elements', 'ArrayExpression'],
|
||||
[1, 'index'],
|
||||
]
|
||||
|
||||
const constraints: (ConstrainInfo & { filterValue: string })[] = [
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(interiorArr.elements[0]),
|
||||
value: code.slice(
|
||||
interiorArr.elements[0].start,
|
||||
interiorArr.elements[0].end
|
||||
),
|
||||
stdLibFnName: 'arcTo',
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
key: 'interior',
|
||||
index: 0,
|
||||
},
|
||||
sourceRange: topLevelRange(
|
||||
interiorArr.elements[0].start,
|
||||
interiorArr.elements[0].end
|
||||
),
|
||||
pathToNode: pathToInteriorX,
|
||||
filterValue: 'interior',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(interiorArr.elements[1]),
|
||||
value: code.slice(
|
||||
interiorArr.elements[1].start,
|
||||
interiorArr.elements[1].end
|
||||
),
|
||||
stdLibFnName: 'arcTo',
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
key: 'interior',
|
||||
index: 1,
|
||||
},
|
||||
sourceRange: topLevelRange(
|
||||
interiorArr.elements[1].start,
|
||||
interiorArr.elements[1].end
|
||||
),
|
||||
pathToNode: pathToInteriorY,
|
||||
filterValue: 'interior',
|
||||
},
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(endArr.elements[0]),
|
||||
value: code.slice(endArr.elements[0].start, endArr.elements[0].end),
|
||||
stdLibFnName: 'arcTo',
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
key: 'end',
|
||||
index: 0,
|
||||
},
|
||||
sourceRange: topLevelRange(
|
||||
endArr.elements[0].start,
|
||||
endArr.elements[0].end
|
||||
),
|
||||
pathToNode: pathToEndX,
|
||||
filterValue: 'end',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(endArr.elements[1]),
|
||||
value: code.slice(endArr.elements[1].start, endArr.elements[1].end),
|
||||
stdLibFnName: 'arcTo',
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
key: 'end',
|
||||
index: 1,
|
||||
},
|
||||
sourceRange: topLevelRange(
|
||||
endArr.elements[1].start,
|
||||
endArr.elements[1].end
|
||||
),
|
||||
pathToNode: pathToEndY,
|
||||
filterValue: 'end',
|
||||
},
|
||||
]
|
||||
|
||||
const finalConstraints: ConstrainInfo[] = []
|
||||
constraints.forEach((constraint) => {
|
||||
if (!filterValue) {
|
||||
finalConstraints.push(constraint)
|
||||
}
|
||||
if (filterValue && constraint.filterValue === filterValue) {
|
||||
finalConstraints.push(constraint)
|
||||
}
|
||||
})
|
||||
return finalConstraints
|
||||
},
|
||||
}
|
||||
|
||||
export const circleThreePoint: SketchLineHelperKw = {
|
||||
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
|
||||
if (segmentInput.type !== 'circle-three-point-segment') {
|
||||
@ -2270,6 +2926,8 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
|
||||
angledLineToY,
|
||||
angledLineThatIntersects,
|
||||
tangentialArcTo,
|
||||
arc,
|
||||
arcTo,
|
||||
} as const
|
||||
|
||||
export const sketchLineHelperMapKw: { [key: string]: SketchLineHelperKw } = {
|
||||
@ -2297,6 +2955,7 @@ export function changeSketchArguments(
|
||||
},
|
||||
input: SegmentInputs
|
||||
): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error {
|
||||
// TODO/less-than-ideal, this obvious relies on node getting mutated, as changing the following with `_node = structuredClone(node)` breaks the draft line animation.
|
||||
const _node = { ...node }
|
||||
const thePath =
|
||||
sourceRangeOrPath.type === 'sourceRange'
|
||||
@ -2529,7 +3188,7 @@ export function addCallExpressionsToPipe({
|
||||
pathToNode: PathToNode
|
||||
expressions: Node<CallExpression | CallExpressionKw>[]
|
||||
}) {
|
||||
const _node = { ...node }
|
||||
const _node: Node<Program> = structuredClone(node)
|
||||
const pipeExpression = getNodeFromPath<Node<PipeExpression>>(
|
||||
_node,
|
||||
pathToNode,
|
||||
@ -2548,7 +3207,7 @@ export function addCloseToPipe({
|
||||
node,
|
||||
pathToNode,
|
||||
}: {
|
||||
node: Program
|
||||
node: Node<Program>
|
||||
variables: VariableMap
|
||||
pathToNode: PathToNode
|
||||
}) {
|
||||
|
@ -1351,8 +1351,11 @@ export function getRemoveConstraintsTransform(
|
||||
}
|
||||
|
||||
if (
|
||||
sketchFnExp.type === 'CallExpressionKw' &&
|
||||
sketchFnExp.callee.name === 'circleThreePoint'
|
||||
(sketchFnExp.type === 'CallExpressionKw' &&
|
||||
sketchFnExp.callee.name === 'circleThreePoint') ||
|
||||
(sketchFnExp.type === 'CallExpression' &&
|
||||
(sketchFnExp.callee.name === 'arcTo' ||
|
||||
sketchFnExp.callee.name === 'arc'))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@ -1613,6 +1616,9 @@ function getTransformMapPath(
|
||||
if (!toolTips.includes(name)) {
|
||||
return false
|
||||
}
|
||||
if (name === 'arcTo') {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if the function is locked down and so can't be transformed
|
||||
const firstArg = getFirstArg(sketchFnExp)
|
||||
@ -2093,8 +2099,10 @@ export function transformAstSketchLines({
|
||||
center: seg.center,
|
||||
radius: seg.radius,
|
||||
from,
|
||||
to: from, // For a full circle, to is the same as from
|
||||
ccw: true, // Default to counter-clockwise for circles
|
||||
}
|
||||
: seg.type === 'CircleThreePoint'
|
||||
: seg.type === 'CircleThreePoint' || seg.type === 'ArcThreePoint'
|
||||
? {
|
||||
type: 'circle-three-point-segment',
|
||||
p1: seg.p1,
|
||||
|
@ -42,8 +42,10 @@ interface StraightSegmentInput {
|
||||
interface ArcSegmentInput {
|
||||
type: 'arc-segment'
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
center: [number, number]
|
||||
radius: number
|
||||
ccw: boolean
|
||||
}
|
||||
/** Inputs for three point circle */
|
||||
interface CircleThreePointSegmentInput {
|
||||
@ -98,6 +100,9 @@ export type InputArgKeys =
|
||||
| 'p1'
|
||||
| 'p2'
|
||||
| 'p3'
|
||||
| 'end'
|
||||
| 'interior'
|
||||
| `angle${'Start' | 'End'}`
|
||||
export interface SingleValueInput<T> {
|
||||
type: 'singleValue'
|
||||
argType: LineInputsType
|
||||
|
@ -414,10 +414,22 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
},
|
||||
{
|
||||
id: 'three-point-arc',
|
||||
onClick: () => console.error('Three-point arc not yet implemented'),
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingState.matches({ Sketch: 'Arc three point tool' })
|
||||
? 'arcThreePoint'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
icon: 'arc',
|
||||
status: 'unavailable',
|
||||
status: 'available',
|
||||
title: 'Three-point Arc',
|
||||
hotkey: (state) =>
|
||||
state.matches({ Sketch: 'Arc three point tool' })
|
||||
? ['Esc', 'T']
|
||||
: 'T',
|
||||
showTitle: false,
|
||||
description: 'Draw a circular arc defined by three points',
|
||||
links: [
|
||||
@ -426,6 +438,26 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
url: 'https://github.com/KittyCAD/modeling-app/issues/1659',
|
||||
},
|
||||
],
|
||||
isActive: (state) =>
|
||||
state.matches({ Sketch: 'Arc three point tool' }),
|
||||
},
|
||||
{
|
||||
id: 'arc',
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingState.matches({ Sketch: 'Arc tool' })
|
||||
? 'arc'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
icon: 'arc',
|
||||
status: DEV ? 'available' : 'unavailable',
|
||||
title: 'Arc',
|
||||
description: 'Start drawing an arc',
|
||||
links: [],
|
||||
isActive: (state) => state.matches({ Sketch: 'Arc tool' }),
|
||||
},
|
||||
],
|
||||
{
|
||||
@ -473,7 +505,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
tool: !modelingState.matches({
|
||||
Sketch: 'Circle three point tool',
|
||||
})
|
||||
? 'circleThreePointNeo'
|
||||
? 'circleThreePoint'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
onDragNumberCalculation,
|
||||
hasLeadingZero,
|
||||
hasDigitsLeftOfDecimal,
|
||||
isClockwise,
|
||||
} from './utils'
|
||||
import { SourceRange, topLevelRange } from '../lang/wasm'
|
||||
|
||||
@ -1253,3 +1254,56 @@ describe('testing onDragNumberCalculation', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing isClockwise', () => {
|
||||
it('returns for counter clockwise points', () => {
|
||||
// Points in clockwise order (rectangle)
|
||||
const clockwisePoints: [number, number][] = [
|
||||
[0, 0], // bottom-left
|
||||
[10, 0], // bottom-right
|
||||
[10, 10], // top-right
|
||||
[0, 10], // top-left
|
||||
]
|
||||
expect(isClockwise(clockwisePoints)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true for clockwise points', () => {
|
||||
// Points in counter-clockwise order (rectangle)
|
||||
const counterClockwisePoints: [number, number][] = [
|
||||
[0, 0], // bottom-left
|
||||
[0, 10], // top-left
|
||||
[10, 10], // top-right
|
||||
[10, 0], // bottom-right
|
||||
]
|
||||
expect(isClockwise(counterClockwisePoints)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for less than 3 points', () => {
|
||||
expect(
|
||||
isClockwise([
|
||||
[0, 0],
|
||||
[10, 10],
|
||||
])
|
||||
).toBe(false)
|
||||
expect(isClockwise([[0, 0]])).toBe(false)
|
||||
expect(isClockwise([])).toBe(false)
|
||||
})
|
||||
|
||||
it('correctly identifies counter-clockwise triangle', () => {
|
||||
const clockwiseTriangle: [number, number][] = [
|
||||
[0, 0],
|
||||
[10, 0],
|
||||
[5, 10],
|
||||
]
|
||||
expect(isClockwise(clockwiseTriangle)).toBe(false)
|
||||
})
|
||||
|
||||
it('correctly identifies clockwise triangle', () => {
|
||||
const counterClockwiseTriangle: [number, number][] = [
|
||||
[0, 0],
|
||||
[5, 10],
|
||||
[10, 0],
|
||||
]
|
||||
expect(isClockwise(counterClockwiseTriangle)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
@ -387,3 +387,22 @@ export function onMouseDragMakeANewNumber(
|
||||
if (!newVal) return
|
||||
setText(newVal)
|
||||
}
|
||||
|
||||
export function isClockwise(points: [number, number][]): boolean {
|
||||
// Need at least 3 points to determine orientation
|
||||
if (points.length < 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Calculate the sum of (x2 - x1) * (y2 + y1) for all edges
|
||||
// This is the "shoelace formula" for calculating the signed area
|
||||
let sum = 0
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const current = points[i]
|
||||
const next = points[(i + 1) % points.length]
|
||||
sum += (next[0] - current[0]) * (next[1] + current[1])
|
||||
}
|
||||
|
||||
// If sum is positive, the points are in clockwise order
|
||||
return sum > 0
|
||||
}
|
||||
|