#5339 Add tangent snapping to straight segment tool (#5995)

* first draft of making segment snap to previous arc's last tangent

* ability to force/disable line snap, threshold in screen space

* mouseEvent refactor tsc errors fixed

* cleanups, extract getTanPreviousPoint function

* add snap line support when previous segment is ARC

* small cleanups

* remove unused planeNodePath param from onDragSegment

* renaming

* Enable snapping when placing the segment point in onClick

* refactor getSnappedDragPoint to include axis intersection

* handle snapping to both axis and tangent direction

* snap refinements

* small cleanups

* lint

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* A snapshot a day keeps the bugs away! 📷🐛

* generate tag for previous arc when snapping current straight segment

* using previous arc's tag in snapped angledLine

* angledLine uses object instead of array now

* use more general snap object instead

* snap tangent line visualized when snapping occurs

* remove unused scale param from createLine

* prettier

* fix bug where segment body is not drawn

* fix generated kcl error introduced in merge from main - modifiedAst needs to be passed to addNewSketchLn

* add support for snapping to negative tangent direction

* fix findTangentDirection for THREE_POINT_ARC_SEGMENT

* fix tsc error by introducing overrideExpr

* fix missing ccw for 3 point arc, fix tan_previous_point calculation for 3 point arcs

* resolve clippy until confirmation for circle radius

* fix runtime error when drawing a 3 point arc

* add unit tests to closestPointoOnRay

* unrelated react warning fixed

* add playwright test for tangent snapping

* better fix for tan_previous_point

* fix lint

* add simulation test for tangent_to_3_point_arc

* Fix simulation test output

* Add missing simulation test output files

* fix tangent snapping bug: use current group instead of last group in activeSegments

* make testcombos.test happy

* cleanup merge

* fix merge mistake, tsc error

* update tangent_to_3_point_arc simulation test

* fix angledLine related breaking tests

* minimum distance added before snapping to tangent

* circle is always ccw regardless of the order of points for tangential info calculation

* fix snapping when different unit is used other than mm

* update test: Straight line snapping to previous tangent

* update rust snapshot test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
This commit is contained in:
Andrew Varga
2025-04-14 23:51:14 +02:00
committed by GitHub
parent add1b21503
commit d0e9b111af
26 changed files with 1556 additions and 129 deletions

View File

@ -205,6 +205,11 @@ export class ToolbarFixture {
).toBeVisible() ).toBeVisible()
await this.page.getByTestId('dropdown-three-point-arc').click() await this.page.getByTestId('dropdown-three-point-arc').click()
} }
selectLine = async () => {
await this.page
.getByRole('button', { name: 'line Line', exact: true })
.click()
}
async closePane(paneId: SidebarType) { async closePane(paneId: SidebarType) {
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX) return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)

View File

@ -3034,4 +3034,78 @@ test.describe('Redirecting to home page and back to the original file should cle
await homePage.openProject('testDefault') await homePage.openProject('testDefault')
await expect(page.getByText('323.49')).not.toBeVisible() await expect(page.getByText('323.49')).not.toBeVisible()
}) })
test('Straight line snapping to previous tangent', async ({
page,
homePage,
toolbar,
scene,
cmdBar,
context,
editor,
}) => {
await context.addInitScript(() => {
localStorage.setItem('persistCode', `@settings(defaultLengthUnit = mm)`)
})
const viewportSize = { width: 1200, height: 900 }
await page.setBodyDimensions(viewportSize)
await homePage.goToModelingScene()
// wait until scene is ready to be interacted with
await scene.connectionEstablished()
await scene.settled(cmdBar)
await page.getByRole('button', { name: 'Start Sketch' }).click()
// select an axis plane
await page.mouse.click(700, 200)
// Needed as we don't yet have a way to get a signal from the engine that the camera has animated to the sketch plane
await page.waitForTimeout(3000)
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
const { click00r } = getMovementUtils({ center, page })
// Draw line
await click00r(0, 0)
await click00r(200, -200)
// Draw arc
await toolbar.tangentialArcBtn.click()
await click00r(0, 0)
await click00r(100, 100)
// Switch back to line
await toolbar.selectLine()
await click00r(0, 0)
await click00r(-100, 100)
// Draw a 3 point arc
await toolbar.selectThreePointArc()
await click00r(0, 0)
await click00r(0, 100)
await click00r(100, 0)
// draw a line to opposite tangnet direction of previous arc
await toolbar.selectLine()
await click00r(0, 0)
await click00r(-200, 200)
await editor.expectEditor.toContain(
`@settings(defaultLengthUnit = mm)
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([0, 0], sketch001)
|> line(end = [191.39, 191.39])
|> tangentialArc(endAbsolute = [287.08, 95.69], tag = $seg01)
|> angledLine(angle = tangentToEnd(seg01), length = 135.34)
|> arcTo({
interior = [191.39, -95.69],
end = [287.08, -95.69]
}, %, $seg02)
|> angledLine(angle = tangentToEnd(seg02) + turns::HALF_TURN, length = 270.67)
`.replaceAll('\n', '')
)
})
}) })

View File

@ -644,7 +644,7 @@ impl GetTangentialInfoFromPathsResult {
pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] { pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
match self { match self {
GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p, GetTangentialInfoFromPathsResult::PreviousPoint(p) => *p,
GetTangentialInfoFromPathsResult::Arc { center, ccw, .. } => { GetTangentialInfoFromPathsResult::Arc { center, ccw } => {
crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end) crate::std::utils::get_tangent_point_from_previous_arc(*center, *ccw, last_arc_end)
} }
// The circle always starts at 0 degrees, so a suitable tangent // The circle always starts at 0 degrees, so a suitable tangent
@ -1231,12 +1231,9 @@ impl Path {
}, },
Path::ArcThreePoint { p1, p2, p3, .. } => { Path::ArcThreePoint { p1, p2, p3, .. } => {
let circle_center = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]); let circle_center = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]);
let radius = linear_distance(&[circle_center.center[0], circle_center.center[1]], p1); GetTangentialInfoFromPathsResult::Arc {
let center_point = [circle_center.center[0], circle_center.center[1]]; center: circle_center.center,
GetTangentialInfoFromPathsResult::Circle { ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
center: center_point,
ccw: true,
radius,
} }
} }
Path::Circle { Path::Circle {
@ -1252,6 +1249,7 @@ impl Path {
let center_point = [circle_center.center[0], circle_center.center[1]]; let center_point = [circle_center.center[0], circle_center.center[1]];
GetTangentialInfoFromPathsResult::Circle { GetTangentialInfoFromPathsResult::Circle {
center: center_point, center: center_point,
// Note: a circle is always ccw regardless of the order of points
ccw: true, ccw: true,
radius, radius,
} }

View File

@ -2482,6 +2482,7 @@ mod intersect_cubes {
super::execute(TEST_NAME, true).await super::execute(TEST_NAME, true).await
} }
} }
mod pattern_into_union { mod pattern_into_union {
const TEST_NAME: &str = "pattern_into_union"; const TEST_NAME: &str = "pattern_into_union";
@ -2524,3 +2525,24 @@ mod subtract_doesnt_need_brackets {
super::execute(TEST_NAME, true).await super::execute(TEST_NAME, true).await
} }
} }
mod tangent_to_3_point_arc {
const TEST_NAME: &str = "tangent_to_3_point_arc";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -0,0 +1,156 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands tangent_to_3_point_arc.kcl
---
[
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "edge_lines_visible",
"hidden": false
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "object_visible",
"object_id": "[uuid]",
"hidden": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "make_plane",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"x_axis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"y_axis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"size": 60.0,
"clobber": false,
"hide": true
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "enable_sketch_mode",
"entity_id": "[uuid]",
"ortho": false,
"animated": false,
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": -1.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "start_path"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": 100.0,
"y": 0.0,
"z": 0.0
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "sketch_mode_disable"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": 0.0,
"y": 120.0,
"z": 0.0
},
"relative": true
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "arc_to",
"interior": {
"x": 300.0,
"y": 100.0,
"z": 0.0
},
"end": {
"x": 200.0,
"y": -100.0,
"z": 0.0
},
"relative": false
}
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "extend_path",
"path": "[uuid]",
"segment": {
"type": "line",
"end": {
"x": -99.8038,
"y": -6.2608,
"z": 0.0
},
"relative": true
}
}
}
]

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart tangent_to_3_point_arc.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,14 @@
```mermaid
flowchart LR
subgraph path2 [Path]
2["Path<br>[43, 82, 0]"]
3["Segment<br>[88, 112, 0]"]
4["Segment<br>[118, 209, 0]"]
5["Segment<br>[215, 292, 0]"]
end
1["Plane<br>[12, 29, 0]"]
1 --- 2
2 --- 3
2 --- 4
2 --- 5
```

View File

@ -0,0 +1,490 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing tangent_to_3_point_arc.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "sketch001",
"start": 0,
"type": "Identifier"
},
"init": {
"arguments": [
{
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "XZ",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "startSketchOn",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "profile001",
"start": 0,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"commentStart": 0,
"elements": [
{
"commentStart": 0,
"end": 0,
"raw": "100.0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 100.0,
"suffix": "None"
}
},
{
"commentStart": 0,
"end": 0,
"raw": "0.0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "sketch001",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "startProfileAt",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "end",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"elements": [
{
"commentStart": 0,
"end": 0,
"raw": "0.0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
},
{
"commentStart": 0,
"end": 0,
"raw": "120.0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 120.0,
"suffix": "None"
}
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "line",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
{
"arguments": [
{
"commentStart": 0,
"end": 0,
"properties": [
{
"commentStart": 0,
"end": 0,
"key": {
"commentStart": 0,
"end": 0,
"name": "interior",
"start": 0,
"type": "Identifier"
},
"start": 0,
"type": "ObjectProperty",
"value": {
"commentStart": 0,
"elements": [
{
"commentStart": 0,
"end": 0,
"raw": "300.0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 300.0,
"suffix": "None"
}
},
{
"commentStart": 0,
"end": 0,
"raw": "100.0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 100.0,
"suffix": "None"
}
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
},
{
"commentStart": 0,
"end": 0,
"key": {
"commentStart": 0,
"end": 0,
"name": "end",
"start": 0,
"type": "Identifier"
},
"start": 0,
"type": "ObjectProperty",
"value": {
"commentStart": 0,
"elements": [
{
"commentStart": 0,
"end": 0,
"raw": "200.00",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 200.0,
"suffix": "None"
}
},
{
"argument": {
"commentStart": 0,
"end": 0,
"raw": "100.00",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 100.0,
"suffix": "None"
}
},
"commentStart": 0,
"end": 0,
"operator": "-",
"start": 0,
"type": "UnaryExpression",
"type": "UnaryExpression"
}
],
"end": 0,
"start": 0,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"start": 0,
"type": "ObjectExpression",
"type": "ObjectExpression"
},
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
},
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "TagDeclarator",
"type": "TagDeclarator",
"value": "seg01"
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "arcTo",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "angle",
"start": 0,
"type": "Identifier"
},
"arg": {
"arguments": [
{
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "seg01",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name",
"type": "Name"
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "tangentToEnd",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpression",
"type": "CallExpression"
}
},
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "length",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "100.00",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 100.0,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "angledLine",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
}
],
"commentStart": 0,
"end": 0,
"start": 0,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"commentStart": 0,
"end": 0,
"start": 0
}
}

View File

@ -0,0 +1,11 @@
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([100.0, 0.0], sketch001)
|> line(end = [0.0, 120.0])
|> arcTo({
interior = [300.0, 100.0],
end = [200.00, -100.00]
}, %, $seg01)
|> angledLine(
angle = tangentToEnd(seg01),
length = 100.00
)

View File

@ -0,0 +1,21 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed tangent_to_3_point_arc.kcl
---
[
{
"labeledArgs": {
"planeOrSolid": {
"value": {
"type": "Plane",
"artifact_id": "[uuid]"
},
"sourceRange": []
}
},
"name": "startSketchOn",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": null
}
]

View File

@ -0,0 +1,208 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing tangent_to_3_point_arc.kcl
---
{
"profile001": {
"type": "Sketch",
"value": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
100.0,
0.0
],
"tag": null,
"to": [
100.0,
120.0
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
100.0,
120.0
],
"p1": [
100.0,
120.0
],
"p2": [
300.0,
100.0
],
"p3": [
200.0,
-100.0
],
"tag": {
"commentStart": 202,
"end": 208,
"start": 202,
"type": "TagDeclarator",
"value": "seg01"
},
"to": [
200.0,
-100.0
],
"type": "ArcThreePoint",
"units": {
"type": "Mm"
}
},
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
},
"from": [
200.0,
-100.0
],
"tag": null,
"to": [
100.1962,
-106.2608
],
"type": "ToPoint",
"units": {
"type": "Mm"
}
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XZ",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"yAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"units": {
"type": "Mm"
}
},
"start": {
"from": [
100.0,
0.0
],
"to": [
100.0,
0.0
],
"units": {
"type": "Mm"
},
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": []
}
},
"tags": {
"seg01": {
"type": "TagIdentifier",
"value": "seg01"
}
},
"artifactId": "[uuid]",
"originalId": "[uuid]",
"units": {
"type": "Mm"
}
}
},
"seg01": {
"type": "TagIdentifier",
"type": "TagIdentifier",
"value": "seg01"
},
"sketch001": {
"type": "Plane",
"value": {
"id": "[uuid]",
"artifactId": "[uuid]",
"value": "XZ",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"yAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"units": {
"type": "Mm"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,12 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing tangent_to_3_point_arc.kcl
---
sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([100.0, 0.0], sketch001)
|> line(end = [0.0, 120.0])
|> arcTo({
interior = [300.0, 100.0],
end = [200.0, -100.0]
}, %, $seg01)
|> angledLine(angle = tangentToEnd(seg01), length = 100.0)

View File

@ -9,6 +9,7 @@ export const ARC_SEGMENT_DASH = 'arc-segment-dash'
export const STRAIGHT_SEGMENT = 'straight-segment' export const STRAIGHT_SEGMENT = 'straight-segment'
export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body' export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body'
export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed' export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed'
export const STRAIGHT_SEGMENT_SNAP_LINE = 'straight-segment-snap-line'
export const CIRCLE_SEGMENT = 'circle-segment' export const CIRCLE_SEGMENT = 'circle-segment'
export const CIRCLE_SEGMENT_BODY = 'circle-segment-body' export const CIRCLE_SEGMENT_BODY = 'circle-segment-body'
export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed' export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed'
@ -62,6 +63,12 @@ export const SEGMENT_BODIES_PLUS_PROFILE_START = [
PROFILE_START, PROFILE_START,
] ]
export const ARC_SEGMENT_TYPES = [
TANGENTIAL_ARC_TO_SEGMENT,
THREE_POINT_ARC_SEGMENT,
ARC_SEGMENT,
]
// Helper functions // Helper functions
export function getParentGroup( export function getParentGroup(
object: any, object: any,

View File

@ -5,6 +5,7 @@ import type {
Object3DEventMap, Object3DEventMap,
Quaternion, Quaternion,
} from 'three' } from 'three'
import { import {
BoxGeometry, BoxGeometry,
DoubleSide, DoubleSide,
@ -36,7 +37,8 @@ import type { Sketch } from '@rust/kcl-lib/bindings/Sketch'
import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange' import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange'
import type { VariableDeclaration } from '@rust/kcl-lib/bindings/VariableDeclaration' import type { VariableDeclaration } from '@rust/kcl-lib/bindings/VariableDeclaration'
import type { VariableDeclarator } from '@rust/kcl-lib/bindings/VariableDeclarator' import type { VariableDeclarator } from '@rust/kcl-lib/bindings/VariableDeclarator'
import { uuidv4 } from '@src/lib/utils' import type { SafeArray } from '@src/lib/utils'
import { getAngle, getLength, uuidv4 } from '@src/lib/utils'
import { import {
createGridHelper, createGridHelper,
@ -48,6 +50,7 @@ import {
import { import {
ARC_ANGLE_END, ARC_ANGLE_END,
ARC_SEGMENT, ARC_SEGMENT,
ARC_SEGMENT_TYPES,
CIRCLE_CENTER_HANDLE, CIRCLE_CENTER_HANDLE,
CIRCLE_SEGMENT, CIRCLE_SEGMENT,
CIRCLE_THREE_POINT_HANDLE1, CIRCLE_THREE_POINT_HANDLE1,
@ -56,6 +59,7 @@ import {
CIRCLE_THREE_POINT_SEGMENT, CIRCLE_THREE_POINT_SEGMENT,
DRAFT_DASHED_LINE, DRAFT_DASHED_LINE,
EXTRA_SEGMENT_HANDLE, EXTRA_SEGMENT_HANDLE,
getParentGroup,
PROFILE_START, PROFILE_START,
SEGMENT_BODIES, SEGMENT_BODIES,
SEGMENT_BODIES_PLUS_PROFILE_START, SEGMENT_BODIES_PLUS_PROFILE_START,
@ -66,31 +70,32 @@ import {
THREE_POINT_ARC_HANDLE2, THREE_POINT_ARC_HANDLE2,
THREE_POINT_ARC_HANDLE3, THREE_POINT_ARC_HANDLE3,
THREE_POINT_ARC_SEGMENT, THREE_POINT_ARC_SEGMENT,
getParentGroup,
} from '@src/clientSideScene/sceneConstants' } from '@src/clientSideScene/sceneConstants'
import type { import type {
OnClickCallbackArgs, OnClickCallbackArgs,
OnMouseEnterLeaveArgs, OnMouseEnterLeaveArgs,
SceneInfra, SceneInfra,
} from '@src/clientSideScene/sceneInfra' } from '@src/clientSideScene/sceneInfra'
import { import {
ANGLE_SNAP_THRESHOLD_DEGREES, ANGLE_SNAP_THRESHOLD_DEGREES,
ARROWHEAD, ARROWHEAD,
AXIS_GROUP, AXIS_GROUP,
DRAFT_POINT, DRAFT_POINT,
DRAFT_POINT_GROUP, DRAFT_POINT_GROUP,
getSceneScale,
INTERSECTION_PLANE_LAYER, INTERSECTION_PLANE_LAYER,
RAYCASTABLE_PLANE, RAYCASTABLE_PLANE,
SKETCH_GROUP_SEGMENTS, SKETCH_GROUP_SEGMENTS,
SKETCH_LAYER, SKETCH_LAYER,
X_AXIS, X_AXIS,
Y_AXIS, Y_AXIS,
getSceneScale,
} from '@src/clientSideScene/sceneUtils' } from '@src/clientSideScene/sceneUtils'
import type { SegmentUtils } from '@src/clientSideScene/segments' import type { SegmentUtils } from '@src/clientSideScene/segments'
import { import {
createProfileStartHandle, createProfileStartHandle,
dashedStraight, dashedStraight,
getTanPreviousPoint,
segmentUtils, segmentUtils,
} from '@src/clientSideScene/segments' } from '@src/clientSideScene/segments'
import type EditorManager from '@src/editor/manager' import type EditorManager from '@src/editor/manager'
@ -118,6 +123,7 @@ import {
insertNewStartProfileAt, insertNewStartProfileAt,
updateSketchNodePathsWithInsertIndex, updateSketchNodePathsWithInsertIndex,
} from '@src/lang/modifyAst' } from '@src/lang/modifyAst'
import { mutateAstWithTagForSketchSegment } from '@src/lang/modifyAst/addEdgeTreatment'
import { getNodeFromPath } from '@src/lang/queryAst' import { getNodeFromPath } from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import { import {
@ -138,6 +144,7 @@ import { topLevelRange } from '@src/lang/util'
import type { PathToNode, VariableMap } from '@src/lang/wasm' import type { PathToNode, VariableMap } from '@src/lang/wasm'
import { import {
defaultSourceRange, defaultSourceRange,
getTangentialArcToInfo,
parse, parse,
recast, recast,
resultIsOk, resultIsOk,
@ -157,12 +164,14 @@ import type { Themes } from '@src/lib/theme'
import { getThemeColorForThreeJs } from '@src/lib/theme' import { getThemeColorForThreeJs } from '@src/lib/theme'
import { err, reportRejection, trap } from '@src/lib/trap' import { err, reportRejection, trap } from '@src/lib/trap'
import { isArray, isOverlap, roundOff } from '@src/lib/utils' import { isArray, isOverlap, roundOff } from '@src/lib/utils'
import { closestPointOnRay, deg2Rad } from '@src/lib/utils2d'
import type { import type {
SegmentOverlayPayload, SegmentOverlayPayload,
SketchDetails, SketchDetails,
SketchDetailsUpdate, SketchDetailsUpdate,
SketchTool, SketchTool,
} from '@src/machines/modelingMachine' } from '@src/machines/modelingMachine'
import { calculateIntersectionOfTwoLines } from 'sketch-helpers'
type DraftSegment = 'line' | 'tangentialArc' type DraftSegment = 'line' | 'tangentialArc'
@ -183,6 +192,7 @@ export class SceneEntities {
axisGroup: Group | null = null axisGroup: Group | null = null
draftPointGroups: Group[] = [] draftPointGroups: Group[] = []
currentSketchQuaternion: Quaternion | null = null currentSketchQuaternion: Quaternion | null = null
constructor( constructor(
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
sceneInfra: SceneInfra, sceneInfra: SceneInfra,
@ -344,6 +354,7 @@ export class SceneEntities {
sceneInfra.scene.add(intersectionPlane) sceneInfra.scene.add(intersectionPlane)
return intersectionPlane return intersectionPlane
} }
createSketchAxis( createSketchAxis(
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
forward: [number, number, number], forward: [number, number, number],
@ -423,9 +434,11 @@ export class SceneEntities {
sketchPosition && this.axisGroup.position.set(...sketchPosition) sketchPosition && this.axisGroup.position.set(...sketchPosition)
this.sceneInfra.scene.add(this.axisGroup) this.sceneInfra.scene.add(this.axisGroup)
} }
getDraftPoint() { getDraftPoint() {
return this.sceneInfra.scene.getObjectByName(DRAFT_POINT) return this.sceneInfra.scene.getObjectByName(DRAFT_POINT)
} }
createDraftPoint({ createDraftPoint({
point, point,
origin, origin,
@ -857,6 +870,7 @@ export class SceneEntities {
variableDeclarationName, variableDeclarationName,
} }
} }
updateAstAndRejigSketch = async ( updateAstAndRejigSketch = async (
sketchEntryNodePath: PathToNode, sketchEntryNodePath: PathToNode,
sketchNodePaths: PathToNode[], sketchNodePaths: PathToNode[],
@ -1016,22 +1030,25 @@ export class SceneEntities {
}) })
if (trap(modifiedAst)) return Promise.reject(modifiedAst) if (trap(modifiedAst)) return Promise.reject(modifiedAst)
} else if (intersection2d) { } else if (intersection2d) {
const intersectsYAxis = args.intersects.find( const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
(sceneObject) => sceneObject.object.name === Y_AXIS
) let {
const intersectsXAxis = args.intersects.find( snappedPoint,
(sceneObject) => sceneObject.object.name === X_AXIS snappedToTangent,
intersectsXAxis,
intersectsYAxis,
negativeTangentDirection,
} = this.getSnappedDragPoint(
intersection2d,
args.intersects,
args.mouseEvent,
Object.values(this.activeSegments).at(-1)
) )
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
const snappedPoint = {
x: intersectsYAxis ? 0 : intersection2d.x,
y: intersectsXAxis ? 0 : intersection2d.y,
}
// Get the angle between the previous segment (or sketch start)'s end and this one's // Get the angle between the previous segment (or sketch start)'s end and this one's
const angle = Math.atan2( const angle = Math.atan2(
snappedPoint.y - lastSegment.to[1], snappedPoint[1] - lastSegment.to[1],
snappedPoint.x - lastSegment.to[0] snappedPoint[0] - lastSegment.to[0]
) )
const isHorizontal = const isHorizontal =
@ -1043,6 +1060,12 @@ export class SceneEntities {
ANGLE_SNAP_THRESHOLD_DEGREES ANGLE_SNAP_THRESHOLD_DEGREES
let resolvedFunctionName: ToolTip = 'line' let resolvedFunctionName: ToolTip = 'line'
const snaps = {
previousArcTag: '',
negativeTangentDirection,
xAxis: !!intersectsXAxis,
yAxis: !!intersectsYAxis,
}
// This might need to become its own function if we want more // This might need to become its own function if we want more
// case-based logic for different segment types // case-based logic for different segment types
@ -1051,27 +1074,46 @@ export class SceneEntities {
segmentName === 'tangentialArc' segmentName === 'tangentialArc'
) { ) {
resolvedFunctionName = 'tangentialArc' resolvedFunctionName = 'tangentialArc'
} else if (snappedToTangent) {
// Generate tag for previous arc segment and use it for the angle of angledLine:
// |> tangentialArcTo([5, -10], %, $arc001)
// |> angledLine({ angle = tangentToEnd(arc001), length = 12 }, %)
const previousSegmentPathToNode = getNodePathFromSourceRange(
modifiedAst,
sourceRangeFromRust(lastSegment.__geoMeta.sourceRange)
)
const taggedAstResult = mutateAstWithTagForSketchSegment(
modifiedAst,
previousSegmentPathToNode
)
if (trap(taggedAstResult)) return Promise.reject(taggedAstResult)
modifiedAst = taggedAstResult.modifiedAst
snaps.previousArcTag = taggedAstResult.tag
resolvedFunctionName = 'angledLine'
} else if (isHorizontal) { } else if (isHorizontal) {
// If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine // If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine
resolvedFunctionName = 'xLine' resolvedFunctionName = 'xLine'
} else if (isVertical) { } else if (isVertical) {
// If the angle between is 90 or 270 degrees (+/- the snapping angle), make the line a yLine // If the angle between is 90 or 270 degrees (+/- the snapping angle), make the line a yLine
resolvedFunctionName = 'yLine' resolvedFunctionName = 'yLine'
} else if (snappedPoint.x === 0 || snappedPoint.y === 0) { } else if (snappedPoint[0] === 0 || snappedPoint[1] === 0) {
// We consider a point placed on axes or origin to be absolute // We consider a point placed on axes or origin to be absolute
resolvedFunctionName = 'lineTo' resolvedFunctionName = 'lineTo'
} }
const tmp = addNewSketchLn({ const tmp = addNewSketchLn({
node: this.kclManager.ast, node: modifiedAst,
variables: this.kclManager.variables, variables: this.kclManager.variables,
input: { input: {
type: 'straight-segment', type: 'straight-segment',
from: [lastSegment.to[0], lastSegment.to[1]], from: [lastSegment.to[0], lastSegment.to[1]],
to: [snappedPoint.x, snappedPoint.y], to: [snappedPoint[0], snappedPoint[1]],
}, },
fnName: resolvedFunctionName, fnName: resolvedFunctionName,
pathToNode: sketchEntryNodePath, pathToNode: sketchEntryNodePath,
snaps,
}) })
if (trap(tmp)) return Promise.reject(tmp) if (trap(tmp)) return Promise.reject(tmp)
modifiedAst = tmp.modifiedAst modifiedAst = tmp.modifiedAst
@ -1118,11 +1160,11 @@ export class SceneEntities {
intersects: args.intersects, intersects: args.intersects,
sketchNodePaths, sketchNodePaths,
sketchEntryNodePath, sketchEntryNodePath,
planeNodePath,
draftInfo: { draftInfo: {
truncatedAst, truncatedAst,
variableDeclarationName, variableDeclarationName,
}, },
mouseEvent: args.mouseEvent,
}) })
}, },
}) })
@ -1263,10 +1305,11 @@ export class SceneEntities {
const { intersectionPoint } = args const { intersectionPoint } = args
if (!intersectionPoint?.twoD) return if (!intersectionPoint?.twoD) return
const { snappedPoint, isSnapped } = this.getSnappedDragPoint({ const { snappedPoint, isSnapped } = this.getSnappedDragPoint(
intersection2d: intersectionPoint.twoD, intersectionPoint.twoD,
intersects: args.intersects, args.intersects,
}) args.mouseEvent
)
if (isSnapped) { if (isSnapped) {
this.positionDraftPoint({ this.positionDraftPoint({
snappedPoint: new Vector2(...snappedPoint), snappedPoint: new Vector2(...snappedPoint),
@ -2046,10 +2089,11 @@ export class SceneEntities {
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node.declaration.init const sketchInit = _node.node.declaration.init
const maybeSnapToAxis = this.getSnappedDragPoint({ const maybeSnapToAxis = this.getSnappedDragPoint(
intersection2d: args.intersectionPoint.twoD, args.intersectionPoint.twoD,
intersects: args.intersects, args.intersects,
}).snappedPoint args.mouseEvent
).snappedPoint
const maybeSnapToProfileStart = doNotSnapAsThreePointArcIsTheOnlySegment const maybeSnapToProfileStart = doNotSnapAsThreePointArcIsTheOnlySegment
? new Vector2(...maybeSnapToAxis) ? new Vector2(...maybeSnapToAxis)
@ -2148,10 +2192,11 @@ export class SceneEntities {
type: 'circle-three-point-segment', type: 'circle-three-point-segment',
p1, p1,
p2, p2,
p3: this.getSnappedDragPoint({ p3: this.getSnappedDragPoint(
intersection2d: args.intersectionPoint.twoD, args.intersectionPoint.twoD,
intersects: args.intersects, args.intersects,
}).snappedPoint, args.mouseEvent
).snappedPoint,
} }
) )
if (err(moddedResult)) return if (err(moddedResult)) return
@ -2524,10 +2569,10 @@ export class SceneEntities {
this.onDragSegment({ this.onDragSegment({
sketchNodePaths, sketchNodePaths,
sketchEntryNodePath: pathToNodeForNewSegment, sketchEntryNodePath: pathToNodeForNewSegment,
planeNodePath,
object: selected, object: selected,
intersection2d: intersectionPoint.twoD, intersection2d: intersectionPoint.twoD,
intersects, intersects,
mouseEvent: mouseEvent,
}) })
} }
return return
@ -2536,10 +2581,10 @@ export class SceneEntities {
this.onDragSegment({ this.onDragSegment({
object: selected, object: selected,
intersection2d: intersectionPoint.twoD, intersection2d: intersectionPoint.twoD,
planeNodePath,
intersects, intersects,
sketchNodePaths, sketchNodePaths,
sketchEntryNodePath, sketchEntryNodePath,
mouseEvent: mouseEvent,
}) })
}, },
onMove: () => {}, onMove: () => {},
@ -2578,13 +2623,19 @@ export class SceneEntities {
this.kclManager.lastSuccessfulVariables, this.kclManager.lastSuccessfulVariables,
draftSegment draftSegment
) )
getSnappedDragPoint({
intersects, getSnappedDragPoint(
intersection2d, pos: Vector2,
}: { intersects: Intersection<Object3D<Object3DEventMap>>[],
intersects: Intersection<Object3D<Object3DEventMap>>[] mouseEvent: MouseEvent,
intersection2d: Vector2 // During draft segment mouse move:
}): { snappedPoint: [number, number]; isSnapped: boolean } { // - the three.js object currently being dragged: the new draft segment or existing segment (may not be the last in activeSegments)
// When placing the draft segment::
// - the last segment in activeSegments
currentObject?: Object3D | Group
) {
let snappedPoint: Coords2d = [pos.x, pos.y]
const intersectsYAxis = intersects.find( const intersectsYAxis = intersects.find(
(sceneObject) => sceneObject.object.name === Y_AXIS (sceneObject) => sceneObject.object.name === Y_AXIS
) )
@ -2592,16 +2643,116 @@ export class SceneEntities {
(sceneObject) => sceneObject.object.name === X_AXIS (sceneObject) => sceneObject.object.name === X_AXIS
) )
const snappedPoint = new Vector2( // Snap to previous segment's tangent direction when drawing a straight segment
intersectsYAxis ? 0 : intersection2d.x, let snappedToTangent = false
intersectsXAxis ? 0 : intersection2d.y let negativeTangentDirection = false
)
const disableTangentSnapping = mouseEvent.ctrlKey || mouseEvent.altKey
const forceDirectionSnapping = mouseEvent.shiftKey
if (!disableTangentSnapping) {
const segments: SafeArray<Group> = Object.values(this.activeSegments) // Using the order in the object feels wrong
const currentIndex =
currentObject instanceof Group ? segments.indexOf(currentObject) : -1
const current = segments[currentIndex]
if (
current?.userData.type === STRAIGHT_SEGMENT &&
// This draft check is not strictly necessary currently, but we only want
// to snap when drawing a new segment, this makes that more robust.
current?.userData.draft
) {
const prev = segments[currentIndex - 1]
if (prev && ARC_SEGMENT_TYPES.includes(prev.userData.type)) {
const snapDirection = findTangentDirection(prev)
if (snapDirection) {
const SNAP_TOLERANCE_PIXELS = 12 * window.devicePixelRatio
const SNAP_MIN_DISTANCE_PIXELS = 5 * window.devicePixelRatio
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
// See if snapDirection intersects with any of the axes
if (intersectsXAxis || intersectsYAxis) {
let intersectionPoint: Coords2d | undefined
if (intersectsXAxis && intersectsYAxis) {
// Current mouse position intersects with both axes (origin) -> that has precedence over tangent so we snap to the origin.
intersectionPoint = [0, 0]
} else {
// Intersects only one axis
const axisLine: [Coords2d, Coords2d] = intersectsXAxis
? [
[0, 0],
[1, 0],
]
: [
[0, 0],
[0, 1],
]
// See if that axis line intersects with the tangent direction
// Note: this includes both positive and negative tangent directions as it just checks 2 lines.
intersectionPoint = calculateIntersectionOfTwoLines({
line1: axisLine,
line2Angle: getAngle([0, 0], snapDirection),
line2Point: current.userData.from,
})
}
// If yes, see if that intersection point is within tolerance and if yes snap to it.
if (
intersectionPoint &&
getLength(intersectionPoint, snappedPoint) / orthoFactor <
SNAP_TOLERANCE_PIXELS
) {
snappedPoint = intersectionPoint
snappedToTangent = true
}
}
if (!snappedToTangent) {
// Otherwise, try to snap to the tangent direction, in both positive and negative directions
const { closestPoint, t } = closestPointOnRay(
prev.userData.to,
snapDirection,
snappedPoint,
true
)
if (
forceDirectionSnapping ||
(this.sceneInfra.screenSpaceDistance(
closestPoint,
snappedPoint
) < SNAP_TOLERANCE_PIXELS &&
// We only want to snap to the tangent direction if the mouse has moved enough to avoid quick jumps
// at the beginning of the drag
this.sceneInfra.screenSpaceDistance(
current.userData.from,
current.userData.to
) > SNAP_MIN_DISTANCE_PIXELS)
) {
snappedPoint = closestPoint
snappedToTangent = true
negativeTangentDirection = t < 0
}
}
}
}
}
}
// Snap to the main axes if there was no snapping to tangent direction
if (!snappedToTangent) {
snappedPoint = [
intersectsYAxis ? 0 : snappedPoint[0],
intersectsXAxis ? 0 : snappedPoint[1],
]
}
return { return {
snappedPoint: [snappedPoint.x, snappedPoint.y], isSnapped: !!(intersectsYAxis || intersectsXAxis || snappedToTangent),
isSnapped: !!(intersectsYAxis || intersectsXAxis), snappedToTangent,
negativeTangentDirection,
snappedPoint,
intersectsXAxis,
intersectsYAxis,
} }
} }
positionDraftPoint({ positionDraftPoint({
origin, origin,
yAxis, yAxis,
@ -2634,6 +2785,7 @@ export class SceneEntities {
draftPoint.position.set(snappedPoint.x, snappedPoint.y, 0) draftPoint.position.set(snappedPoint.x, snappedPoint.y, 0)
} }
} }
maybeSnapProfileStartIntersect2d({ maybeSnapProfileStartIntersect2d({
sketchEntryNodePath, sketchEntryNodePath,
intersects, intersects,
@ -2654,25 +2806,26 @@ export class SceneEntities {
: _intersection2d : _intersection2d
return intersection2d return intersection2d
} }
onDragSegment({ onDragSegment({
object, object,
intersection2d: _intersection2d, intersection2d: _intersection2d,
sketchEntryNodePath, sketchEntryNodePath,
sketchNodePaths, sketchNodePaths,
planeNodePath,
draftInfo, draftInfo,
intersects, intersects,
mouseEvent,
}: { }: {
object: Object3D<Object3DEventMap> object: Object3D<Object3DEventMap>
intersection2d: Vector2 intersection2d: Vector2
sketchEntryNodePath: PathToNode sketchEntryNodePath: PathToNode
sketchNodePaths: PathToNode[] sketchNodePaths: PathToNode[]
planeNodePath: PathToNode
intersects: Intersection<Object3D<Object3DEventMap>>[] intersects: Intersection<Object3D<Object3DEventMap>>[]
draftInfo?: { draftInfo?: {
truncatedAst: Node<Program> truncatedAst: Node<Program>
variableDeclarationName: string variableDeclarationName: string
} }
mouseEvent: MouseEvent
}) { }) {
const intersection2d = this.maybeSnapProfileStartIntersect2d({ const intersection2d = this.maybeSnapProfileStartIntersect2d({
sketchEntryNodePath, sketchEntryNodePath,
@ -2705,10 +2858,12 @@ export class SceneEntities {
group.userData?.from?.[0], group.userData?.from?.[0],
group.userData?.from?.[1], group.userData?.from?.[1],
] ]
const dragTo = this.getSnappedDragPoint({ const { snappedPoint: dragTo, snappedToTangent } = this.getSnappedDragPoint(
intersects,
intersection2d, intersection2d,
}).snappedPoint intersects,
mouseEvent,
object
)
let modifiedAst = draftInfo let modifiedAst = draftInfo
? draftInfo.truncatedAst ? draftInfo.truncatedAst
: { ...this.kclManager.ast } : { ...this.kclManager.ast }
@ -2938,7 +3093,8 @@ export class SceneEntities {
varDecIndex, varDecIndex,
modifiedAst, modifiedAst,
orthoFactor, orthoFactor,
sketch sketch,
snappedToTangent
) )
callBacks.push( callBacks.push(
@ -2949,7 +3105,8 @@ export class SceneEntities {
varDecIndex, varDecIndex,
modifiedAst, modifiedAst,
orthoFactor, orthoFactor,
sketch sketch,
snappedToTangent
) )
) )
) )
@ -2967,6 +3124,7 @@ export class SceneEntities {
* @param modifiedAst * @param modifiedAst
* @param orthoFactor * @param orthoFactor
* @param sketch * @param sketch
* @param snappedToTangent if currently drawn draft segment is snapping to previous arc tangent
*/ */
updateSegment = ( updateSegment = (
segment: Path | Sketch['start'], segment: Path | Sketch['start'],
@ -2974,7 +3132,8 @@ export class SceneEntities {
varDecIndex: number, varDecIndex: number,
modifiedAst: Program, modifiedAst: Program,
orthoFactor: number, orthoFactor: number,
sketch: Sketch sketch: Sketch,
snappedToTangent: boolean = false
): (() => SegmentOverlayPayload | null) => { ): (() => SegmentOverlayPayload | null) => {
const segPathToNode = getNodePathFromSourceRange( const segPathToNode = getNodePathFromSourceRange(
modifiedAst, modifiedAst,
@ -2998,6 +3157,7 @@ export class SceneEntities {
type: 'straight-segment', type: 'straight-segment',
from: segment.from, from: segment.from,
to: segment.to, to: segment.to,
snap: snappedToTangent,
} }
let update: SegmentUtils['update'] | null = null let update: SegmentUtils['update'] | null = null
if (type === TANGENTIAL_ARC_TO_SEGMENT) { if (type === TANGENTIAL_ARC_TO_SEGMENT) {
@ -3095,9 +3255,11 @@ export class SceneEntities {
}) })
}) })
} }
removeSketchGrid() { removeSketchGrid() {
if (this.axisGroup) this.sceneInfra.scene.remove(this.axisGroup) if (this.axisGroup) this.sceneInfra.scene.remove(this.axisGroup)
} }
tearDownSketch({ removeAxis = true }: { removeAxis?: boolean }) { tearDownSketch({ removeAxis = true }: { removeAxis?: boolean }) {
// Remove all draft groups // Remove all draft groups
this.draftPointGroups.forEach((draftPointGroup) => { this.draftPointGroups.forEach((draftPointGroup) => {
@ -3125,6 +3287,7 @@ export class SceneEntities {
this.sceneInfra.camControls.enableRotate = true this.sceneInfra.camControls.enableRotate = true
this.activeSegments = {} this.activeSegments = {}
} }
mouseEnterLeaveCallbacks() { mouseEnterLeaveCallbacks() {
return { return {
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => { onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
@ -3333,6 +3496,7 @@ export class SceneEntities {
}, },
} }
} }
resetOverlays() { resetOverlays() {
this.sceneInfra.modelingSend({ this.sceneInfra.modelingSend({
type: 'Set Segment Overlays', type: 'Set Segment Overlays',
@ -3713,6 +3877,7 @@ function getSketchesInfo({
} }
return sketchesInfo return sketchesInfo
} }
/** /**
* Given a SourceRange [x,y,boolean] create a Selections object which contains * Given a SourceRange [x,y,boolean] create a Selections object which contains
* graphSelections with the artifact and codeRef. * graphSelections with the artifact and codeRef.
@ -3747,3 +3912,36 @@ function isGroupStartProfileForCurrentProfile(sketchEntryNodePath: PathToNode) {
return isProfileStartOfCurrentExpr return isProfileStartOfCurrentExpr
} }
} }
// Returns the 2D tangent direction vector at the end of the segmentGroup if it's an arc.
function findTangentDirection(segmentGroup: Group) {
let tangentDirection: Coords2d | undefined
if (segmentGroup.userData.type === TANGENTIAL_ARC_TO_SEGMENT) {
const prevSegment = segmentGroup.userData.prevSegment
const arcInfo = getTangentialArcToInfo({
arcStartPoint: segmentGroup.userData.from,
arcEndPoint: segmentGroup.userData.to,
tanPreviousPoint: getTanPreviousPoint(prevSegment),
obtuse: true,
})
const tangentAngle =
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
} else if (
segmentGroup.userData.type === ARC_SEGMENT ||
segmentGroup.userData.type === THREE_POINT_ARC_SEGMENT
) {
const tangentAngle =
deg2Rad(
getAngle(segmentGroup.userData.center, segmentGroup.userData.to)
) +
(Math.PI / 2) * (segmentGroup.userData.ccw ? 1 : -1)
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
} else {
console.warn(
'Unsupported segment type for tangent direction calculation: ',
segmentGroup.userData.type
)
}
return tangentDirection
}

View File

@ -2,7 +2,6 @@ import * as TWEEN from '@tweenjs/tween.js'
import type { import type {
Group, Group,
Intersection, Intersection,
Mesh,
MeshBasicMaterial, MeshBasicMaterial,
Object3D, Object3D,
Object3DEventMap, Object3DEventMap,
@ -13,6 +12,7 @@ import {
Color, Color,
GridHelper, GridHelper,
LineBasicMaterial, LineBasicMaterial,
Mesh,
OrthographicCamera, OrthographicCamera,
Raycaster, Raycaster,
Scene, Scene,
@ -42,7 +42,7 @@ import { compareVec2Epsilon2 } from '@src/lang/std/sketch'
import type { Axis, NonCodeSelection } from '@src/lib/selections' import type { Axis, NonCodeSelection } from '@src/lib/selections'
import { type BaseUnit } from '@src/lib/settings/settingsTypes' import { type BaseUnit } from '@src/lib/settings/settingsTypes'
import { Themes } from '@src/lib/theme' import { Themes } from '@src/lib/theme'
import { getAngle, throttle } from '@src/lib/utils' import { getAngle, getLength, throttle } from '@src/lib/utils'
import type { import type {
MouseState, MouseState,
SegmentOverlayPayload, SegmentOverlayPayload,
@ -68,6 +68,7 @@ interface OnDragCallbackArgs extends OnMouseEnterLeaveArgs {
} }
intersects: Intersection<Object3D<Object3DEventMap>>[] intersects: Intersection<Object3D<Object3DEventMap>>[]
} }
export interface OnClickCallbackArgs { export interface OnClickCallbackArgs {
mouseEvent: MouseEvent mouseEvent: MouseEvent
intersectionPoint?: { intersectionPoint?: {
@ -93,6 +94,7 @@ interface OnMoveCallbackArgs {
// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts // Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
type Voidish = void | Promise<void> type Voidish = void | Promise<void>
export class SceneInfra { export class SceneInfra {
static instance: SceneInfra static instance: SceneInfra
readonly scene: Scene readonly scene: Scene
@ -130,6 +132,7 @@ export class SceneInfra {
this.onMouseLeave = callbacks.onMouseLeave || this.onMouseLeave this.onMouseLeave = callbacks.onMouseLeave || this.onMouseLeave
this.selected = null // following selections between callbacks being set is too tricky this.selected = null // following selections between callbacks being set is too tricky
} }
set baseUnit(unit: BaseUnit) { set baseUnit(unit: BaseUnit) {
this._baseUnitMultiplier = baseUnitTomm(unit) this._baseUnitMultiplier = baseUnitTomm(unit)
this.scene.scale.set( this.scene.scale.set(
@ -138,9 +141,11 @@ export class SceneInfra {
this._baseUnitMultiplier this._baseUnitMultiplier
) )
} }
set theme(theme: Themes) { set theme(theme: Themes) {
this._theme = theme this._theme = theme
} }
resetMouseListeners = () => { resetMouseListeners = () => {
this.setCallbacks({ this.setCallbacks({
onDragStart: () => {}, onDragStart: () => {},
@ -155,12 +160,15 @@ export class SceneInfra {
modelingSend: SendType = (() => {}) as any modelingSend: SendType = (() => {}) as any
throttledModelingSend: any = (() => {}) as any throttledModelingSend: any = (() => {}) as any
setSend(send: SendType) { setSend(send: SendType) {
this.modelingSend = send this.modelingSend = send
this.throttledModelingSend = throttle(send, 100) this.throttledModelingSend = throttle(send, 100)
} }
overlayTimeout = 0 overlayTimeout = 0
callbacks: (() => SegmentOverlayPayload | null)[] = [] callbacks: (() => SegmentOverlayPayload | null)[] = []
_overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) { _overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) {
const segmentOverlayPayload: SegmentOverlayPayload = { const segmentOverlayPayload: SegmentOverlayPayload = {
type: 'add-many', type: 'add-many',
@ -179,6 +187,7 @@ export class SceneInfra {
data: segmentOverlayPayload, data: segmentOverlayPayload,
}) })
} }
overlayCallbacks( overlayCallbacks(
callbacks: (() => SegmentOverlayPayload | null)[], callbacks: (() => SegmentOverlayPayload | null)[],
instant = false instant = false
@ -195,6 +204,7 @@ export class SceneInfra {
} }
overlayThrottleMap: { [pathToNodeString: string]: number } = {} overlayThrottleMap: { [pathToNodeString: string]: number } = {}
updateOverlayDetails({ updateOverlayDetails({
handle, handle,
group, group,
@ -349,6 +359,7 @@ export class SceneInfra {
window.removeEventListener('resize', this.onWindowResize) window.removeEventListener('resize', this.onWindowResize)
// Dispose of any other resources like geometries, materials, textures // Dispose of any other resources like geometries, materials, textures
} }
getClientSceneScaleFactor(meshOrGroup: Mesh | Group) { getClientSceneScaleFactor(meshOrGroup: Mesh | Group) {
const orthoFactor = orthoScale(this.camControls.camera) const orthoFactor = orthoScale(this.camControls.camera)
const factor = const factor =
@ -358,6 +369,7 @@ export class SceneInfra {
this._baseUnitMultiplier this._baseUnitMultiplier
return factor return factor
} }
getPlaneIntersectPoint = (): { getPlaneIntersectPoint = (): {
twoD?: Vector2 twoD?: Vector2
threeD?: Vector3 threeD?: Vector3
@ -556,6 +568,7 @@ export class SceneInfra {
(a, b) => a.distance - b.distance (a, b) => a.distance - b.distance
) )
} }
updateMouseState(mouseState: MouseState) { updateMouseState(mouseState: MouseState) {
if (this.lastMouseState.type === mouseState.type) return if (this.lastMouseState.type === mouseState.type) return
this.lastMouseState = mouseState this.lastMouseState = mouseState
@ -665,6 +678,13 @@ export class SceneInfra {
} }
}) })
} }
screenSpaceDistance(a: Coords2d, b: Coords2d): number {
const dummy = new Mesh()
dummy.position.set(0, 0, 0)
const scale = this.getClientSceneScaleFactor(dummy)
return getLength(a, b) / scale
}
} }
function baseUnitTomm(baseUnit: BaseUnit) { function baseUnitTomm(baseUnit: BaseUnit) {

View File

@ -54,6 +54,7 @@ import {
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
STRAIGHT_SEGMENT_BODY, STRAIGHT_SEGMENT_BODY,
STRAIGHT_SEGMENT_DASH, STRAIGHT_SEGMENT_DASH,
STRAIGHT_SEGMENT_SNAP_LINE,
TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT_BODY, TANGENTIAL_ARC_TO_SEGMENT_BODY,
TANGENTIAL_ARC_TO__SEGMENT_DASH, TANGENTIAL_ARC_TO__SEGMENT_DASH,
@ -216,7 +217,17 @@ class StraightSegment implements SegmentUtils {
segmentGroup.add(lengthIndicatorGroup) segmentGroup.add(lengthIndicatorGroup)
} }
if (isDraftSegment) {
const snapLine = createLine({
from: [0, 0],
to: [0, 0],
color: 0xcccccc,
})
snapLine.name = STRAIGHT_SEGMENT_SNAP_LINE
segmentGroup.add(snapLine)
}
segmentGroup.add(mesh, extraSegmentGroup) segmentGroup.add(mesh, extraSegmentGroup)
let updateOverlaysCallback = this.update({ let updateOverlaysCallback = this.update({
prevSegment, prevSegment,
input, input,
@ -265,20 +276,39 @@ class StraightSegment implements SegmentUtils {
isHandlesVisible = !shouldHideHover isHandlesVisible = !shouldHideHover
} }
const dir = new Vector3()
.subVectors(
new Vector3(to[0], to[1], 0),
new Vector3(from[0], from[1], 0)
)
.normalize()
if (arrowGroup) { if (arrowGroup) {
arrowGroup.position.set(to[0], to[1], 0) arrowGroup.position.set(to[0], to[1], 0)
const dir = new Vector3()
.subVectors(
new Vector3(to[0], to[1], 0),
new Vector3(from[0], from[1], 0)
)
.normalize()
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir) arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
arrowGroup.scale.set(scale, scale, scale) arrowGroup.scale.set(scale, scale, scale)
arrowGroup.visible = isHandlesVisible arrowGroup.visible = isHandlesVisible
} }
const snapLine = group.getObjectByName(STRAIGHT_SEGMENT_SNAP_LINE) as Line
if (snapLine) {
snapLine.visible = !!input.snap
if (snapLine.visible) {
const snapLineFrom = to
const snapLineTo = new Vector3(to[0], to[1], 0).addScaledVector(
dir,
// Draw a large enough line that reaches the screen edge
// Cleaner way would be to draw in screen space
9999999 * scale
)
updateLine(snapLine, {
from: snapLineFrom,
to: [snapLineTo.x, snapLineTo.y],
scale,
})
}
}
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE) const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
if (extraSegmentGroup) { if (extraSegmentGroup) {
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1]) const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
@ -431,33 +461,10 @@ class TangentialArcToSegment implements SegmentUtils {
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE) const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
let previousPoint = prevSegment.from
if (prevSegment?.type === 'TangentialArcTo') {
previousPoint = getTangentPointFromPreviousArc(
prevSegment.center,
prevSegment.ccw,
prevSegment.to
)
} 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({ const arcInfo = getTangentialArcToInfo({
arcStartPoint: from, arcStartPoint: from,
arcEndPoint: to, arcEndPoint: to,
tanPreviousPoint: previousPoint, tanPreviousPoint: getTanPreviousPoint(prevSegment),
obtuse: true, obtuse: true,
}) })
@ -539,6 +546,32 @@ class TangentialArcToSegment implements SegmentUtils {
} }
} }
export function getTanPreviousPoint(prevSegment: Sketch['paths'][number]) {
let previousPoint = prevSegment.from
if (prevSegment.type === 'TangentialArcTo') {
previousPoint = getTangentPointFromPreviousArc(
prevSegment.center,
prevSegment.ccw,
prevSegment.to
)
} 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
)
}
return previousPoint
}
class CircleSegment implements SegmentUtils { class CircleSegment implements SegmentUtils {
init: SegmentUtils['init'] = ({ init: SegmentUtils['init'] = ({
prevSegment, prevSegment,
@ -1018,21 +1051,18 @@ class ArcSegment implements SegmentUtils {
const centerToFromLine = createLine({ const centerToFromLine = createLine({
from: center, from: center,
to: from, to: from,
scale,
color: grey, // Light gray color for the line color: grey, // Light gray color for the line
}) })
centerToFromLine.name = ARC_CENTER_TO_FROM centerToFromLine.name = ARC_CENTER_TO_FROM
const centerToToLine = createLine({ const centerToToLine = createLine({
from: center, from: center,
to, to,
scale,
color: grey, // Light gray color for the line color: grey, // Light gray color for the line
}) })
centerToToLine.name = ARC_CENTER_TO_TO centerToToLine.name = ARC_CENTER_TO_TO
const angleReferenceLine = createLine({ const angleReferenceLine = createLine({
from: [center[0] + (ANGLE_INDICATOR_RADIUS - 2) * scale, center[1]], from: [center[0] + (ANGLE_INDICATOR_RADIUS - 2) * scale, center[1]],
to: [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 color: grey, // Light gray color for the line
}) })
angleReferenceLine.name = ARC_ANGLE_REFERENCE_LINE angleReferenceLine.name = ARC_ANGLE_REFERENCE_LINE
@ -1398,7 +1428,7 @@ class ThreePointArcSegment implements SegmentUtils {
p3, p3,
radius, radius,
center, center,
ccw: false, ccw: !isClockwise([p1, p2, p3]),
prevSegment, prevSegment,
pathToNode, pathToNode,
isSelected, isSelected,
@ -1992,12 +2022,10 @@ export function dashedStraight(
function createLine({ function createLine({
from, from,
to, to,
scale,
color, color,
}: { }: {
from: [number, number] from: [number, number]
to: [number, number] to: [number, number]
scale: number
color: number color: number
}): Line { }): Line {
// Implementation for creating a line // Implementation for creating a line

View File

@ -312,7 +312,7 @@ export function getPathToExtrudeForSegmentSelection(
export function mutateAstWithTagForSketchSegment( export function mutateAstWithTagForSketchSegment(
astClone: Node<Program>, astClone: Node<Program>,
pathToSegmentNode: PathToNode pathToSegmentNode: PathToNode
): { modifiedAst: Program; tag: string } | Error { ): { modifiedAst: Node<Program>; tag: string } | Error {
const segmentNode = getNodeFromPath<CallExpression | CallExpressionKw>( const segmentNode = getNodeFromPath<CallExpression | CallExpressionKw>(
astClone, astClone,
pathToSegmentNode, pathToSegmentNode,

View File

@ -20,10 +20,12 @@ import {
} from '@src/lang/constants' } from '@src/lang/constants'
import { import {
createArrayExpression, createArrayExpression,
createBinaryExpression,
createCallExpression, createCallExpression,
createCallExpressionStdLibKw, createCallExpressionStdLibKw,
createLabeledArg, createLabeledArg,
createLiteral, createLiteral,
createLocalName,
createObjectExpression, createObjectExpression,
createPipeExpression, createPipeExpression,
createPipeSubstitution, createPipeSubstitution,
@ -60,6 +62,7 @@ import type {
SingleValueInput, SingleValueInput,
SketchLineHelper, SketchLineHelper,
SketchLineHelperKw, SketchLineHelperKw,
addCall,
} from '@src/lang/std/stdTypes' } from '@src/lang/std/stdTypes'
import { import {
findKwArg, findKwArg,
@ -2324,8 +2327,9 @@ export const circleThreePoint: SketchLineHelperKw = {
return finalConstraints return finalConstraints
}, },
} }
export const angledLine: SketchLineHelperKw = { export const angledLine: SketchLineHelperKw = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => { add: ({ node, pathToNode, segmentInput, replaceExistingCallback, snaps }) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const { from, to } = segmentInput const { from, to } = segmentInput
const _node = { ...node } const _node = { ...node }
@ -2334,11 +2338,27 @@ export const angledLine: SketchLineHelperKw = {
if (err(_node1)) return _node1 if (err(_node1)) return _node1
const { node: pipe } = _node1 const { node: pipe } = _node1
const newAngleVal = createLiteral(roundOff(getAngle(from, to), 0)) // When snapping to previous arc's tangent direction, create this expression:
// angledLine({ angle = tangentToEnd(arc001), length = 12 }, %)
// Or if snapping to the negative direction:
// angledLine({ angle = tangentToEnd(arc001) + turns::HALF_TURN, length = 12 }, %)
const newAngleVal = snaps?.previousArcTag
? snaps.negativeTangentDirection
? createBinaryExpression([
createCallExpression('tangentToEnd', [
createLocalName(snaps?.previousArcTag),
]),
'+',
createLocalName('turns::HALF_TURN'),
])
: createCallExpression('tangentToEnd', [
createLocalName(snaps?.previousArcTag),
])
: createLiteral(roundOff(getAngle(from, to), 0))
const newLengthVal = createLiteral(roundOff(getLength(from, to), 2)) const newLengthVal = createLiteral(roundOff(getLength(from, to), 2))
const newLine = createCallExpressionStdLibKw('angledLine', null, [ const newLine = createCallExpressionStdLibKw('angledLine', null, [
createLabeledArg('angle', newAngleVal), createLabeledArg(ARG_ANGLE, newAngleVal),
createLabeledArg('length', newLengthVal), createLabeledArg(ARG_LENGTH, newLengthVal),
]) ])
if (replaceExistingCallback) { if (replaceExistingCallback) {
@ -2348,7 +2368,12 @@ export const angledLine: SketchLineHelperKw = {
type: 'labeledArg', type: 'labeledArg',
key: 'angle', key: 'angle',
argType: 'angle', argType: 'angle',
expr: newAngleVal, // We cannot pass newAngleVal to expr because it is a Node<Literal>.
// We couldn't change that type to be Node<Expr> because there is a lot of code assuming it to be Node<Literal>.
// So we added a new optional overrideExpr which can be Node<Expr> and this is used if present in sketchcombos/createNode().
expr:
newAngleVal.type === 'Literal' ? newAngleVal : createLiteral(''),
overrideExpr: newAngleVal,
}, },
{ {
type: 'labeledArg', type: 'labeledArg',
@ -3309,6 +3334,7 @@ interface CreateLineFnCallArgs {
fnName: ToolTip fnName: ToolTip
pathToNode: PathToNode pathToNode: PathToNode
spliceBetween?: boolean spliceBetween?: boolean
snaps?: addCall['snaps']
} }
export function addNewSketchLn({ export function addNewSketchLn({
@ -3318,6 +3344,7 @@ export function addNewSketchLn({
pathToNode, pathToNode,
input: segmentInput, input: segmentInput,
spliceBetween = false, spliceBetween = false,
snaps,
}: CreateLineFnCallArgs): }: CreateLineFnCallArgs):
| { | {
modifiedAst: Node<Program> modifiedAst: Node<Program>
@ -3347,6 +3374,7 @@ export function addNewSketchLn({
pathToNode, pathToNode,
segmentInput, segmentInput,
spliceBetween, spliceBetween,
snaps,
}) })
} }

View File

@ -1538,20 +1538,22 @@ export function removeSingleConstraint({
} }
if (inputToReplace.type === 'arrayItem') { if (inputToReplace.type === 'arrayItem') {
const values = inputs.map((arg) => { const values = inputs.map((arg) => {
const argExpr = arg.overrideExpr ?? arg.expr
if ( if (
!( !(
(arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') && (arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') &&
arg.index === inputToReplace.index arg.index === inputToReplace.index
) )
) )
return arg.expr return argExpr
const literal = rawArgs.find( const rawArg = rawArgs.find(
(rawValue) => (rawValue) =>
(rawValue.type === 'arrayItem' || (rawValue.type === 'arrayItem' ||
rawValue.type === 'arrayOrObjItem') && rawValue.type === 'arrayOrObjItem') &&
rawValue.index === inputToReplace.index rawValue.index === inputToReplace.index
)?.expr )
return (arg.index === inputToReplace.index && literal) || arg.expr const literal = rawArg?.overrideExpr ?? rawArg?.expr
return (arg.index === inputToReplace.index && literal) || argExpr
}) })
if (callExp.node.type === 'CallExpression') { if (callExp.node.type === 'CallExpression') {
return createStdlibCallExpression( return createStdlibCallExpression(
@ -1589,6 +1591,7 @@ export function removeSingleConstraint({
const objInput: Parameters<typeof createObjectExpression>[0] = {} const objInput: Parameters<typeof createObjectExpression>[0] = {}
const kwArgInput: ReturnType<typeof createLabeledArg>[] = [] const kwArgInput: ReturnType<typeof createLabeledArg>[] = []
inputs.forEach((currentArg) => { inputs.forEach((currentArg) => {
const currentArgExpr = currentArg.overrideExpr ?? currentArg.expr
if ( if (
// should be one of these, return early to make TS happy. // should be one of these, return early to make TS happy.
currentArg.type !== 'objectProperty' && currentArg.type !== 'objectProperty' &&
@ -1619,8 +1622,11 @@ export function removeSingleConstraint({
if (!arrayInput[currentArg.key]) { if (!arrayInput[currentArg.key]) {
arrayInput[currentArg.key] = [] arrayInput[currentArg.key] = []
} }
arrayInput[inputToReplace.key][inputToReplace.index] = const rawLiteralArrayInObjectExpr =
rawLiteralArrayInObject.overrideExpr ??
rawLiteralArrayInObject.expr rawLiteralArrayInObject.expr
arrayInput[inputToReplace.key][inputToReplace.index] =
rawLiteralArrayInObjectExpr
let existingKwgForKey = kwArgInput.find( let existingKwgForKey = kwArgInput.find(
(kwArg) => kwArg.label.name === currentArg.key (kwArg) => kwArg.label.name === currentArg.key
) )
@ -1633,7 +1639,7 @@ export function removeSingleConstraint({
} }
if (existingKwgForKey.arg.type === 'ArrayExpression') { if (existingKwgForKey.arg.type === 'ArrayExpression') {
existingKwgForKey.arg.elements[inputToReplace.index] = existingKwgForKey.arg.elements[inputToReplace.index] =
rawLiteralArrayInObject.expr rawLiteralArrayInObjectExpr
} }
} else if ( } else if (
inputToReplace.type === 'objectProperty' && inputToReplace.type === 'objectProperty' &&
@ -1642,10 +1648,11 @@ export function removeSingleConstraint({
rawLiteralObjProp?.key === inputToReplace.key && rawLiteralObjProp?.key === inputToReplace.key &&
currentArg.key === inputToReplace.key currentArg.key === inputToReplace.key
) { ) {
objInput[inputToReplace.key] = rawLiteralObjProp.expr objInput[inputToReplace.key] =
rawLiteralObjProp.overrideExpr ?? rawLiteralObjProp.expr
} else if (currentArg.type === 'arrayInObject') { } else if (currentArg.type === 'arrayInObject') {
if (!arrayInput[currentArg.key]) arrayInput[currentArg.key] = [] if (!arrayInput[currentArg.key]) arrayInput[currentArg.key] = []
arrayInput[currentArg.key][currentArg.index] = currentArg.expr arrayInput[currentArg.key][currentArg.index] = currentArgExpr
let existingKwgForKey = kwArgInput.find( let existingKwgForKey = kwArgInput.find(
(kwArg) => kwArg.label.name === currentArg.key (kwArg) => kwArg.label.name === currentArg.key
) )
@ -1657,10 +1664,10 @@ export function removeSingleConstraint({
kwArgInput.push(existingKwgForKey) kwArgInput.push(existingKwgForKey)
} }
if (existingKwgForKey.arg.type === 'ArrayExpression') { if (existingKwgForKey.arg.type === 'ArrayExpression') {
existingKwgForKey.arg.elements[currentArg.index] = currentArg.expr existingKwgForKey.arg.elements[currentArg.index] = currentArgExpr
} }
} else if (currentArg.type === 'objectProperty') { } else if (currentArg.type === 'objectProperty') {
objInput[currentArg.key] = currentArg.expr objInput[currentArg.key] = currentArgExpr
} }
}) })
const createObjParam: Parameters<typeof createObjectExpression>[0] = {} const createObjParam: Parameters<typeof createObjectExpression>[0] = {}
@ -1694,7 +1701,7 @@ export function removeSingleConstraint({
return createCallWrapper( return createCallWrapper(
callExp.node.callee.name.name as any, callExp.node.callee.name.name as any,
rawArgs[0].expr, rawArgs[0].overrideExpr ?? rawArgs[0].expr,
tag tag
) )
}, },

View File

@ -44,6 +44,7 @@ interface StraightSegmentInput {
type: 'straight-segment' type: 'straight-segment'
from: [number, number] from: [number, number]
to: [number, number] to: [number, number]
snap?: boolean
} }
/** Inputs for arcs, excluding tangentialArc for reasons explain in the /** Inputs for arcs, excluding tangentialArc for reasons explain in the
@ -92,6 +93,12 @@ export interface addCall extends ModifyAstBase {
) => CreatedSketchExprResult | Error ) => CreatedSketchExprResult | Error
referencedSegment?: Path referencedSegment?: Path
spliceBetween?: boolean spliceBetween?: boolean
snaps?: {
previousArcTag?: string
negativeTangentDirection: boolean
xAxis?: boolean
yAxis?: boolean
}
} }
interface updateArgs extends ModifyAstBase { interface updateArgs extends ModifyAstBase {
@ -121,18 +128,21 @@ export interface SingleValueInput<T> {
type: 'singleValue' type: 'singleValue'
argType: LineInputsType argType: LineInputsType
expr: T expr: T
overrideExpr?: Node<Expr>
} }
export interface ArrayItemInput<T> { export interface ArrayItemInput<T> {
type: 'arrayItem' type: 'arrayItem'
index: 0 | 1 index: 0 | 1
argType: LineInputsType argType: LineInputsType
expr: T expr: T
overrideExpr?: Node<Expr>
} }
export interface ObjectPropertyInput<T> { export interface ObjectPropertyInput<T> {
type: 'objectProperty' type: 'objectProperty'
key: InputArgKeys key: InputArgKeys
argType: LineInputsType argType: LineInputsType
expr: T expr: T
overrideExpr?: Node<Expr>
} }
interface ArrayOrObjItemInput<T> { interface ArrayOrObjItemInput<T> {
@ -141,6 +151,7 @@ interface ArrayOrObjItemInput<T> {
index: 0 | 1 index: 0 | 1
argType: LineInputsType argType: LineInputsType
expr: T expr: T
overrideExpr?: Node<Expr>
} }
interface ArrayInObject<T> { interface ArrayInObject<T> {
@ -149,6 +160,7 @@ interface ArrayInObject<T> {
argType: LineInputsType argType: LineInputsType
index: 0 | 1 index: 0 | 1
expr: T expr: T
overrideExpr?: Node<Expr>
} }
interface LabeledArg<T> { interface LabeledArg<T> {
@ -156,6 +168,7 @@ interface LabeledArg<T> {
key: InputArgKeys key: InputArgKeys
argType: LineInputsType argType: LineInputsType
expr: T expr: T
overrideExpr?: Node<Expr>
} }
type _InputArg<T> = type _InputArg<T> =

View File

@ -23,6 +23,10 @@ export function isArray(val: any): val is unknown[] {
return Array.isArray(val) return Array.isArray(val)
} }
export type SafeArray<T> = Omit<Array<T>, number> & {
[index: number]: T | undefined
}
/** /**
* An alternative to `Object.keys()` that returns an array of keys with types. * An alternative to `Object.keys()` that returns an array of keys with types.
* *

View File

@ -1,6 +1,7 @@
import type { Coords2d } from '@src/lang/std/sketch' import type { Coords2d } from '@src/lang/std/sketch'
import { isPointsCCW } from '@src/lang/wasm' import { isPointsCCW } from '@src/lang/wasm'
import { initPromise } from '@src/lang/wasmUtils' import { initPromise } from '@src/lang/wasmUtils'
import { closestPointOnRay } from '@src/lib/utils2d'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
@ -20,3 +21,72 @@ describe('test isPointsCW', () => {
expect(CW).toBe(-1) expect(CW).toBe(-1)
}) })
}) })
describe('test closestPointOnRay', () => {
test('point lies on ray', () => {
const rayOrigin: Coords2d = [0, 0]
const rayDirection: Coords2d = [1, 0]
const pointToCheck: Coords2d = [7, 0]
const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck)
expect(result.closestPoint).toEqual([7, 0])
expect(result.t).toBe(7)
})
test('point is above ray', () => {
const rayOrigin: Coords2d = [1, 0]
const rayDirection: Coords2d = [1, 0]
const pointToCheck: Coords2d = [7, 7]
const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck)
expect(result.closestPoint).toEqual([7, 0])
expect(result.t).toBe(6)
})
test('point lies behind ray origin and allowNegative=false', () => {
const rayOrigin: Coords2d = [0, 0]
const rayDirection: Coords2d = [1, 0]
const pointToCheck: Coords2d = [-7, 7]
const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck)
expect(result.closestPoint).toEqual([0, 0])
expect(result.t).toBe(0)
})
test('point lies behind ray origin and allowNegative=true', () => {
const rayOrigin: Coords2d = [0, 0]
const rayDirection: Coords2d = [1, 0]
const pointToCheck: Coords2d = [-7, 7]
const result = closestPointOnRay(
rayOrigin,
rayDirection,
pointToCheck,
true
)
expect(result.closestPoint).toEqual([-7, 0])
expect(result.t).toBe(-7)
})
test('diagonal ray and point', () => {
const rayOrigin: Coords2d = [0, 0]
const rayDirection: Coords2d = [1, 1]
const pointToCheck: Coords2d = [3, 4]
const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck)
expect(result.closestPoint[0]).toBeCloseTo(3.5)
expect(result.closestPoint[1]).toBeCloseTo(3.5)
expect(result.t).toBeCloseTo(4.95, 1)
})
test('non-normalized direction vector', () => {
const rayOrigin: Coords2d = [0, 0]
const rayDirection: Coords2d = [2, 2]
const pointToCheck: Coords2d = [3, 4]
const result = closestPointOnRay(rayOrigin, rayDirection, pointToCheck)
expect(result.closestPoint[0]).toBeCloseTo(3.5)
expect(result.closestPoint[1]).toBeCloseTo(3.5)
expect(result.t).toBeCloseTo(4.95, 1)
})
})

View File

@ -17,3 +17,38 @@ export function getTangentPointFromPreviousArc(
Math.sin(deg2Rad(tangentialAngle)) * 10 + lastArcEnd[1], Math.sin(deg2Rad(tangentialAngle)) * 10 + lastArcEnd[1],
] ]
} }
export function closestPointOnRay(
rayOrigin: Coords2d,
rayDirection: Coords2d,
pointToCheck: Coords2d,
allowNegative = false
) {
const dirMagnitude = Math.sqrt(
rayDirection[0] * rayDirection[0] + rayDirection[1] * rayDirection[1]
)
const normalizedDir: Coords2d = [
rayDirection[0] / dirMagnitude,
rayDirection[1] / dirMagnitude,
]
const originToPoint: Coords2d = [
pointToCheck[0] - rayOrigin[0],
pointToCheck[1] - rayOrigin[1],
]
let t =
originToPoint[0] * normalizedDir[0] + originToPoint[1] * normalizedDir[1]
if (!allowNegative) {
t = Math.max(0, t)
}
return {
closestPoint: [
rayOrigin[0] + normalizedDir[0] * t,
rayOrigin[1] + normalizedDir[1] * t,
] as Coords2d,
t,
}
}

View File

@ -866,10 +866,11 @@ export const modelingMachine = setup({
if (twoD) { if (twoD) {
sceneInfra.modelingSend({ sceneInfra.modelingSend({
type: 'click in scene', type: 'click in scene',
data: sceneEntitiesManager.getSnappedDragPoint({ data: sceneEntitiesManager.getSnappedDragPoint(
intersection2d: twoD, twoD,
intersects: args.intersects, args.intersects,
}).snappedPoint, args.mouseEvent
).snappedPoint,
}) })
} else { } else {
console.error('No intersection point found') console.error('No intersection point found')
@ -1238,10 +1239,11 @@ export const modelingMachine = setup({
if (!intersectionPoint?.twoD) return if (!intersectionPoint?.twoD) return
if (!context.sketchDetails) return if (!context.sketchDetails) return
const { snappedPoint, isSnapped } = const { snappedPoint, isSnapped } =
sceneEntitiesManager.getSnappedDragPoint({ sceneEntitiesManager.getSnappedDragPoint(
intersection2d: intersectionPoint.twoD, intersectionPoint.twoD,
intersects: args.intersects, args.intersects,
}) args.mouseEvent
)
if (isSnapped) { if (isSnapped) {
sceneEntitiesManager.positionDraftPoint({ sceneEntitiesManager.positionDraftPoint({
snappedPoint: new Vector2(...snappedPoint), snappedPoint: new Vector2(...snappedPoint),

View File

@ -82,16 +82,14 @@ const SignIn = () => {
style={ style={
isDesktop() isDesktop()
? ({ ? ({
'-webkit-app-region': 'drag', WebkitAppRegion: 'drag',
} as CSSProperties) } as CSSProperties)
: {} : {}
} }
> >
<div <div
style={ style={
isDesktop() isDesktop() ? ({ WebkitAppRegion: 'no-drag' } as CSSProperties) : {}
? ({ '-webkit-app-region': 'no-drag' } as CSSProperties)
: {}
} }
className="body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto" className="body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto"
> >