* 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:
@ -205,6 +205,11 @@ export class ToolbarFixture {
|
||||
).toBeVisible()
|
||||
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) {
|
||||
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)
|
||||
|
@ -3034,4 +3034,78 @@ test.describe('Redirecting to home page and back to the original file should cle
|
||||
await homePage.openProject('testDefault')
|
||||
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', '')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -644,7 +644,7 @@ impl GetTangentialInfoFromPathsResult {
|
||||
pub(crate) fn tan_previous_point(&self, last_arc_end: [f64; 2]) -> [f64; 2] {
|
||||
match self {
|
||||
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)
|
||||
}
|
||||
// The circle always starts at 0 degrees, so a suitable tangent
|
||||
@ -1231,12 +1231,9 @@ impl Path {
|
||||
},
|
||||
Path::ArcThreePoint { 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);
|
||||
let center_point = [circle_center.center[0], circle_center.center[1]];
|
||||
GetTangentialInfoFromPathsResult::Circle {
|
||||
center: center_point,
|
||||
ccw: true,
|
||||
radius,
|
||||
GetTangentialInfoFromPathsResult::Arc {
|
||||
center: circle_center.center,
|
||||
ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0,
|
||||
}
|
||||
}
|
||||
Path::Circle {
|
||||
@ -1252,6 +1249,7 @@ impl Path {
|
||||
let center_point = [circle_center.center[0], circle_center.center[1]];
|
||||
GetTangentialInfoFromPathsResult::Circle {
|
||||
center: center_point,
|
||||
// Note: a circle is always ccw regardless of the order of points
|
||||
ccw: true,
|
||||
radius,
|
||||
}
|
||||
|
@ -2482,6 +2482,7 @@ mod intersect_cubes {
|
||||
super::execute(TEST_NAME, true).await
|
||||
}
|
||||
}
|
||||
|
||||
mod 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
156
rust/kcl-lib/tests/tangent_to_3_point_arc/artifact_commands.snap
Normal file
156
rust/kcl-lib/tests/tangent_to_3_point_arc/artifact_commands.snap
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
@ -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
|
||||
---
|
@ -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
|
||||
```
|
490
rust/kcl-lib/tests/tangent_to_3_point_arc/ast.snap
Normal file
490
rust/kcl-lib/tests/tangent_to_3_point_arc/ast.snap
Normal 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
|
||||
}
|
||||
}
|
11
rust/kcl-lib/tests/tangent_to_3_point_arc/input.kcl
Normal file
11
rust/kcl-lib/tests/tangent_to_3_point_arc/input.kcl
Normal 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
|
||||
)
|
21
rust/kcl-lib/tests/tangent_to_3_point_arc/ops.snap
Normal file
21
rust/kcl-lib/tests/tangent_to_3_point_arc/ops.snap
Normal 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
|
||||
}
|
||||
]
|
208
rust/kcl-lib/tests/tangent_to_3_point_arc/program_memory.snap
Normal file
208
rust/kcl-lib/tests/tangent_to_3_point_arc/program_memory.snap
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
rust/kcl-lib/tests/tangent_to_3_point_arc/rendered_model.png
Normal file
BIN
rust/kcl-lib/tests/tangent_to_3_point_arc/rendered_model.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
12
rust/kcl-lib/tests/tangent_to_3_point_arc/unparsed.snap
Normal file
12
rust/kcl-lib/tests/tangent_to_3_point_arc/unparsed.snap
Normal 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)
|
@ -9,6 +9,7 @@ export const ARC_SEGMENT_DASH = 'arc-segment-dash'
|
||||
export const STRAIGHT_SEGMENT = 'straight-segment'
|
||||
export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body'
|
||||
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_BODY = 'circle-segment-body'
|
||||
export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed'
|
||||
@ -62,6 +63,12 @@ export const SEGMENT_BODIES_PLUS_PROFILE_START = [
|
||||
PROFILE_START,
|
||||
]
|
||||
|
||||
export const ARC_SEGMENT_TYPES = [
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
THREE_POINT_ARC_SEGMENT,
|
||||
ARC_SEGMENT,
|
||||
]
|
||||
|
||||
// Helper functions
|
||||
export function getParentGroup(
|
||||
object: any,
|
||||
|
@ -5,6 +5,7 @@ import type {
|
||||
Object3DEventMap,
|
||||
Quaternion,
|
||||
} from 'three'
|
||||
|
||||
import {
|
||||
BoxGeometry,
|
||||
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 { VariableDeclaration } from '@rust/kcl-lib/bindings/VariableDeclaration'
|
||||
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 {
|
||||
createGridHelper,
|
||||
@ -48,6 +50,7 @@ import {
|
||||
import {
|
||||
ARC_ANGLE_END,
|
||||
ARC_SEGMENT,
|
||||
ARC_SEGMENT_TYPES,
|
||||
CIRCLE_CENTER_HANDLE,
|
||||
CIRCLE_SEGMENT,
|
||||
CIRCLE_THREE_POINT_HANDLE1,
|
||||
@ -56,6 +59,7 @@ import {
|
||||
CIRCLE_THREE_POINT_SEGMENT,
|
||||
DRAFT_DASHED_LINE,
|
||||
EXTRA_SEGMENT_HANDLE,
|
||||
getParentGroup,
|
||||
PROFILE_START,
|
||||
SEGMENT_BODIES,
|
||||
SEGMENT_BODIES_PLUS_PROFILE_START,
|
||||
@ -66,31 +70,32 @@ import {
|
||||
THREE_POINT_ARC_HANDLE2,
|
||||
THREE_POINT_ARC_HANDLE3,
|
||||
THREE_POINT_ARC_SEGMENT,
|
||||
getParentGroup,
|
||||
} from '@src/clientSideScene/sceneConstants'
|
||||
import type {
|
||||
OnClickCallbackArgs,
|
||||
OnMouseEnterLeaveArgs,
|
||||
SceneInfra,
|
||||
} from '@src/clientSideScene/sceneInfra'
|
||||
|
||||
import {
|
||||
ANGLE_SNAP_THRESHOLD_DEGREES,
|
||||
ARROWHEAD,
|
||||
AXIS_GROUP,
|
||||
DRAFT_POINT,
|
||||
DRAFT_POINT_GROUP,
|
||||
getSceneScale,
|
||||
INTERSECTION_PLANE_LAYER,
|
||||
RAYCASTABLE_PLANE,
|
||||
SKETCH_GROUP_SEGMENTS,
|
||||
SKETCH_LAYER,
|
||||
X_AXIS,
|
||||
Y_AXIS,
|
||||
getSceneScale,
|
||||
} from '@src/clientSideScene/sceneUtils'
|
||||
import type { SegmentUtils } from '@src/clientSideScene/segments'
|
||||
import {
|
||||
createProfileStartHandle,
|
||||
dashedStraight,
|
||||
getTanPreviousPoint,
|
||||
segmentUtils,
|
||||
} from '@src/clientSideScene/segments'
|
||||
import type EditorManager from '@src/editor/manager'
|
||||
@ -118,6 +123,7 @@ import {
|
||||
insertNewStartProfileAt,
|
||||
updateSketchNodePathsWithInsertIndex,
|
||||
} from '@src/lang/modifyAst'
|
||||
import { mutateAstWithTagForSketchSegment } from '@src/lang/modifyAst/addEdgeTreatment'
|
||||
import { getNodeFromPath } from '@src/lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||
import {
|
||||
@ -138,6 +144,7 @@ import { topLevelRange } from '@src/lang/util'
|
||||
import type { PathToNode, VariableMap } from '@src/lang/wasm'
|
||||
import {
|
||||
defaultSourceRange,
|
||||
getTangentialArcToInfo,
|
||||
parse,
|
||||
recast,
|
||||
resultIsOk,
|
||||
@ -157,12 +164,14 @@ import type { Themes } from '@src/lib/theme'
|
||||
import { getThemeColorForThreeJs } from '@src/lib/theme'
|
||||
import { err, reportRejection, trap } from '@src/lib/trap'
|
||||
import { isArray, isOverlap, roundOff } from '@src/lib/utils'
|
||||
import { closestPointOnRay, deg2Rad } from '@src/lib/utils2d'
|
||||
import type {
|
||||
SegmentOverlayPayload,
|
||||
SketchDetails,
|
||||
SketchDetailsUpdate,
|
||||
SketchTool,
|
||||
} from '@src/machines/modelingMachine'
|
||||
import { calculateIntersectionOfTwoLines } from 'sketch-helpers'
|
||||
|
||||
type DraftSegment = 'line' | 'tangentialArc'
|
||||
|
||||
@ -183,6 +192,7 @@ export class SceneEntities {
|
||||
axisGroup: Group | null = null
|
||||
draftPointGroups: Group[] = []
|
||||
currentSketchQuaternion: Quaternion | null = null
|
||||
|
||||
constructor(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
sceneInfra: SceneInfra,
|
||||
@ -344,6 +354,7 @@ export class SceneEntities {
|
||||
sceneInfra.scene.add(intersectionPlane)
|
||||
return intersectionPlane
|
||||
}
|
||||
|
||||
createSketchAxis(
|
||||
sketchPathToNode: PathToNode,
|
||||
forward: [number, number, number],
|
||||
@ -423,9 +434,11 @@ export class SceneEntities {
|
||||
sketchPosition && this.axisGroup.position.set(...sketchPosition)
|
||||
this.sceneInfra.scene.add(this.axisGroup)
|
||||
}
|
||||
|
||||
getDraftPoint() {
|
||||
return this.sceneInfra.scene.getObjectByName(DRAFT_POINT)
|
||||
}
|
||||
|
||||
createDraftPoint({
|
||||
point,
|
||||
origin,
|
||||
@ -857,6 +870,7 @@ export class SceneEntities {
|
||||
variableDeclarationName,
|
||||
}
|
||||
}
|
||||
|
||||
updateAstAndRejigSketch = async (
|
||||
sketchEntryNodePath: PathToNode,
|
||||
sketchNodePaths: PathToNode[],
|
||||
@ -1016,22 +1030,25 @@ export class SceneEntities {
|
||||
})
|
||||
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
||||
} else if (intersection2d) {
|
||||
const intersectsYAxis = args.intersects.find(
|
||||
(sceneObject) => sceneObject.object.name === Y_AXIS
|
||||
)
|
||||
const intersectsXAxis = args.intersects.find(
|
||||
(sceneObject) => sceneObject.object.name === X_AXIS
|
||||
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
|
||||
|
||||
let {
|
||||
snappedPoint,
|
||||
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
|
||||
const angle = Math.atan2(
|
||||
snappedPoint.y - lastSegment.to[1],
|
||||
snappedPoint.x - lastSegment.to[0]
|
||||
snappedPoint[1] - lastSegment.to[1],
|
||||
snappedPoint[0] - lastSegment.to[0]
|
||||
)
|
||||
|
||||
const isHorizontal =
|
||||
@ -1043,6 +1060,12 @@ export class SceneEntities {
|
||||
ANGLE_SNAP_THRESHOLD_DEGREES
|
||||
|
||||
let resolvedFunctionName: ToolTip = 'line'
|
||||
const snaps = {
|
||||
previousArcTag: '',
|
||||
negativeTangentDirection,
|
||||
xAxis: !!intersectsXAxis,
|
||||
yAxis: !!intersectsYAxis,
|
||||
}
|
||||
|
||||
// This might need to become its own function if we want more
|
||||
// case-based logic for different segment types
|
||||
@ -1051,27 +1074,46 @@ export class SceneEntities {
|
||||
segmentName === '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) {
|
||||
// If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine
|
||||
resolvedFunctionName = 'xLine'
|
||||
} else if (isVertical) {
|
||||
// If the angle between is 90 or 270 degrees (+/- the snapping angle), make the line a 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
|
||||
resolvedFunctionName = 'lineTo'
|
||||
}
|
||||
|
||||
const tmp = addNewSketchLn({
|
||||
node: this.kclManager.ast,
|
||||
node: modifiedAst,
|
||||
variables: this.kclManager.variables,
|
||||
input: {
|
||||
type: 'straight-segment',
|
||||
from: [lastSegment.to[0], lastSegment.to[1]],
|
||||
to: [snappedPoint.x, snappedPoint.y],
|
||||
to: [snappedPoint[0], snappedPoint[1]],
|
||||
},
|
||||
fnName: resolvedFunctionName,
|
||||
pathToNode: sketchEntryNodePath,
|
||||
snaps,
|
||||
})
|
||||
if (trap(tmp)) return Promise.reject(tmp)
|
||||
modifiedAst = tmp.modifiedAst
|
||||
@ -1118,11 +1160,11 @@ export class SceneEntities {
|
||||
intersects: args.intersects,
|
||||
sketchNodePaths,
|
||||
sketchEntryNodePath,
|
||||
planeNodePath,
|
||||
draftInfo: {
|
||||
truncatedAst,
|
||||
variableDeclarationName,
|
||||
},
|
||||
mouseEvent: args.mouseEvent,
|
||||
})
|
||||
},
|
||||
})
|
||||
@ -1263,10 +1305,11 @@ export class SceneEntities {
|
||||
|
||||
const { intersectionPoint } = args
|
||||
if (!intersectionPoint?.twoD) return
|
||||
const { snappedPoint, isSnapped } = this.getSnappedDragPoint({
|
||||
intersection2d: intersectionPoint.twoD,
|
||||
intersects: args.intersects,
|
||||
})
|
||||
const { snappedPoint, isSnapped } = this.getSnappedDragPoint(
|
||||
intersectionPoint.twoD,
|
||||
args.intersects,
|
||||
args.mouseEvent
|
||||
)
|
||||
if (isSnapped) {
|
||||
this.positionDraftPoint({
|
||||
snappedPoint: new Vector2(...snappedPoint),
|
||||
@ -2046,10 +2089,11 @@ export class SceneEntities {
|
||||
if (trap(_node)) return
|
||||
const sketchInit = _node.node.declaration.init
|
||||
|
||||
const maybeSnapToAxis = this.getSnappedDragPoint({
|
||||
intersection2d: args.intersectionPoint.twoD,
|
||||
intersects: args.intersects,
|
||||
}).snappedPoint
|
||||
const maybeSnapToAxis = this.getSnappedDragPoint(
|
||||
args.intersectionPoint.twoD,
|
||||
args.intersects,
|
||||
args.mouseEvent
|
||||
).snappedPoint
|
||||
|
||||
const maybeSnapToProfileStart = doNotSnapAsThreePointArcIsTheOnlySegment
|
||||
? new Vector2(...maybeSnapToAxis)
|
||||
@ -2148,10 +2192,11 @@ export class SceneEntities {
|
||||
type: 'circle-three-point-segment',
|
||||
p1,
|
||||
p2,
|
||||
p3: this.getSnappedDragPoint({
|
||||
intersection2d: args.intersectionPoint.twoD,
|
||||
intersects: args.intersects,
|
||||
}).snappedPoint,
|
||||
p3: this.getSnappedDragPoint(
|
||||
args.intersectionPoint.twoD,
|
||||
args.intersects,
|
||||
args.mouseEvent
|
||||
).snappedPoint,
|
||||
}
|
||||
)
|
||||
if (err(moddedResult)) return
|
||||
@ -2524,10 +2569,10 @@ export class SceneEntities {
|
||||
this.onDragSegment({
|
||||
sketchNodePaths,
|
||||
sketchEntryNodePath: pathToNodeForNewSegment,
|
||||
planeNodePath,
|
||||
object: selected,
|
||||
intersection2d: intersectionPoint.twoD,
|
||||
intersects,
|
||||
mouseEvent: mouseEvent,
|
||||
})
|
||||
}
|
||||
return
|
||||
@ -2536,10 +2581,10 @@ export class SceneEntities {
|
||||
this.onDragSegment({
|
||||
object: selected,
|
||||
intersection2d: intersectionPoint.twoD,
|
||||
planeNodePath,
|
||||
intersects,
|
||||
sketchNodePaths,
|
||||
sketchEntryNodePath,
|
||||
mouseEvent: mouseEvent,
|
||||
})
|
||||
},
|
||||
onMove: () => {},
|
||||
@ -2578,13 +2623,19 @@ export class SceneEntities {
|
||||
this.kclManager.lastSuccessfulVariables,
|
||||
draftSegment
|
||||
)
|
||||
getSnappedDragPoint({
|
||||
intersects,
|
||||
intersection2d,
|
||||
}: {
|
||||
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
||||
intersection2d: Vector2
|
||||
}): { snappedPoint: [number, number]; isSnapped: boolean } {
|
||||
|
||||
getSnappedDragPoint(
|
||||
pos: Vector2,
|
||||
intersects: Intersection<Object3D<Object3DEventMap>>[],
|
||||
mouseEvent: MouseEvent,
|
||||
// During draft segment mouse move:
|
||||
// - 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(
|
||||
(sceneObject) => sceneObject.object.name === Y_AXIS
|
||||
)
|
||||
@ -2592,16 +2643,116 @@ export class SceneEntities {
|
||||
(sceneObject) => sceneObject.object.name === X_AXIS
|
||||
)
|
||||
|
||||
const snappedPoint = new Vector2(
|
||||
intersectsYAxis ? 0 : intersection2d.x,
|
||||
intersectsXAxis ? 0 : intersection2d.y
|
||||
// Snap to previous segment's tangent direction when drawing a straight segment
|
||||
let snappedToTangent = false
|
||||
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 {
|
||||
snappedPoint: [snappedPoint.x, snappedPoint.y],
|
||||
isSnapped: !!(intersectsYAxis || intersectsXAxis),
|
||||
isSnapped: !!(intersectsYAxis || intersectsXAxis || snappedToTangent),
|
||||
snappedToTangent,
|
||||
negativeTangentDirection,
|
||||
snappedPoint,
|
||||
intersectsXAxis,
|
||||
intersectsYAxis,
|
||||
}
|
||||
}
|
||||
|
||||
positionDraftPoint({
|
||||
origin,
|
||||
yAxis,
|
||||
@ -2634,6 +2785,7 @@ export class SceneEntities {
|
||||
draftPoint.position.set(snappedPoint.x, snappedPoint.y, 0)
|
||||
}
|
||||
}
|
||||
|
||||
maybeSnapProfileStartIntersect2d({
|
||||
sketchEntryNodePath,
|
||||
intersects,
|
||||
@ -2654,25 +2806,26 @@ export class SceneEntities {
|
||||
: _intersection2d
|
||||
return intersection2d
|
||||
}
|
||||
|
||||
onDragSegment({
|
||||
object,
|
||||
intersection2d: _intersection2d,
|
||||
sketchEntryNodePath,
|
||||
sketchNodePaths,
|
||||
planeNodePath,
|
||||
draftInfo,
|
||||
intersects,
|
||||
mouseEvent,
|
||||
}: {
|
||||
object: Object3D<Object3DEventMap>
|
||||
intersection2d: Vector2
|
||||
sketchEntryNodePath: PathToNode
|
||||
sketchNodePaths: PathToNode[]
|
||||
planeNodePath: PathToNode
|
||||
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
||||
draftInfo?: {
|
||||
truncatedAst: Node<Program>
|
||||
variableDeclarationName: string
|
||||
}
|
||||
mouseEvent: MouseEvent
|
||||
}) {
|
||||
const intersection2d = this.maybeSnapProfileStartIntersect2d({
|
||||
sketchEntryNodePath,
|
||||
@ -2705,10 +2858,12 @@ export class SceneEntities {
|
||||
group.userData?.from?.[0],
|
||||
group.userData?.from?.[1],
|
||||
]
|
||||
const dragTo = this.getSnappedDragPoint({
|
||||
intersects,
|
||||
const { snappedPoint: dragTo, snappedToTangent } = this.getSnappedDragPoint(
|
||||
intersection2d,
|
||||
}).snappedPoint
|
||||
intersects,
|
||||
mouseEvent,
|
||||
object
|
||||
)
|
||||
let modifiedAst = draftInfo
|
||||
? draftInfo.truncatedAst
|
||||
: { ...this.kclManager.ast }
|
||||
@ -2938,7 +3093,8 @@ export class SceneEntities {
|
||||
varDecIndex,
|
||||
modifiedAst,
|
||||
orthoFactor,
|
||||
sketch
|
||||
sketch,
|
||||
snappedToTangent
|
||||
)
|
||||
|
||||
callBacks.push(
|
||||
@ -2949,7 +3105,8 @@ export class SceneEntities {
|
||||
varDecIndex,
|
||||
modifiedAst,
|
||||
orthoFactor,
|
||||
sketch
|
||||
sketch,
|
||||
snappedToTangent
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -2967,6 +3124,7 @@ export class SceneEntities {
|
||||
* @param modifiedAst
|
||||
* @param orthoFactor
|
||||
* @param sketch
|
||||
* @param snappedToTangent if currently drawn draft segment is snapping to previous arc tangent
|
||||
*/
|
||||
updateSegment = (
|
||||
segment: Path | Sketch['start'],
|
||||
@ -2974,7 +3132,8 @@ export class SceneEntities {
|
||||
varDecIndex: number,
|
||||
modifiedAst: Program,
|
||||
orthoFactor: number,
|
||||
sketch: Sketch
|
||||
sketch: Sketch,
|
||||
snappedToTangent: boolean = false
|
||||
): (() => SegmentOverlayPayload | null) => {
|
||||
const segPathToNode = getNodePathFromSourceRange(
|
||||
modifiedAst,
|
||||
@ -2998,6 +3157,7 @@ export class SceneEntities {
|
||||
type: 'straight-segment',
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
snap: snappedToTangent,
|
||||
}
|
||||
let update: SegmentUtils['update'] | null = null
|
||||
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
||||
@ -3095,9 +3255,11 @@ export class SceneEntities {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
removeSketchGrid() {
|
||||
if (this.axisGroup) this.sceneInfra.scene.remove(this.axisGroup)
|
||||
}
|
||||
|
||||
tearDownSketch({ removeAxis = true }: { removeAxis?: boolean }) {
|
||||
// Remove all draft groups
|
||||
this.draftPointGroups.forEach((draftPointGroup) => {
|
||||
@ -3125,6 +3287,7 @@ export class SceneEntities {
|
||||
this.sceneInfra.camControls.enableRotate = true
|
||||
this.activeSegments = {}
|
||||
}
|
||||
|
||||
mouseEnterLeaveCallbacks() {
|
||||
return {
|
||||
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
|
||||
@ -3333,6 +3496,7 @@ export class SceneEntities {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
resetOverlays() {
|
||||
this.sceneInfra.modelingSend({
|
||||
type: 'Set Segment Overlays',
|
||||
@ -3713,6 +3877,7 @@ function getSketchesInfo({
|
||||
}
|
||||
return sketchesInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a SourceRange [x,y,boolean] create a Selections object which contains
|
||||
* graphSelections with the artifact and codeRef.
|
||||
@ -3747,3 +3912,36 @@ function isGroupStartProfileForCurrentProfile(sketchEntryNodePath: PathToNode) {
|
||||
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
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import * as TWEEN from '@tweenjs/tween.js'
|
||||
import type {
|
||||
Group,
|
||||
Intersection,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
Object3DEventMap,
|
||||
@ -13,6 +12,7 @@ import {
|
||||
Color,
|
||||
GridHelper,
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
OrthographicCamera,
|
||||
Raycaster,
|
||||
Scene,
|
||||
@ -42,7 +42,7 @@ import { compareVec2Epsilon2 } from '@src/lang/std/sketch'
|
||||
import type { Axis, NonCodeSelection } from '@src/lib/selections'
|
||||
import { type BaseUnit } from '@src/lib/settings/settingsTypes'
|
||||
import { Themes } from '@src/lib/theme'
|
||||
import { getAngle, throttle } from '@src/lib/utils'
|
||||
import { getAngle, getLength, throttle } from '@src/lib/utils'
|
||||
import type {
|
||||
MouseState,
|
||||
SegmentOverlayPayload,
|
||||
@ -68,6 +68,7 @@ interface OnDragCallbackArgs extends OnMouseEnterLeaveArgs {
|
||||
}
|
||||
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
||||
}
|
||||
|
||||
export interface OnClickCallbackArgs {
|
||||
mouseEvent: MouseEvent
|
||||
intersectionPoint?: {
|
||||
@ -93,6 +94,7 @@ interface OnMoveCallbackArgs {
|
||||
// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
|
||||
|
||||
type Voidish = void | Promise<void>
|
||||
|
||||
export class SceneInfra {
|
||||
static instance: SceneInfra
|
||||
readonly scene: Scene
|
||||
@ -130,6 +132,7 @@ export class SceneInfra {
|
||||
this.onMouseLeave = callbacks.onMouseLeave || this.onMouseLeave
|
||||
this.selected = null // following selections between callbacks being set is too tricky
|
||||
}
|
||||
|
||||
set baseUnit(unit: BaseUnit) {
|
||||
this._baseUnitMultiplier = baseUnitTomm(unit)
|
||||
this.scene.scale.set(
|
||||
@ -138,9 +141,11 @@ export class SceneInfra {
|
||||
this._baseUnitMultiplier
|
||||
)
|
||||
}
|
||||
|
||||
set theme(theme: Themes) {
|
||||
this._theme = theme
|
||||
}
|
||||
|
||||
resetMouseListeners = () => {
|
||||
this.setCallbacks({
|
||||
onDragStart: () => {},
|
||||
@ -155,12 +160,15 @@ export class SceneInfra {
|
||||
|
||||
modelingSend: SendType = (() => {}) as any
|
||||
throttledModelingSend: any = (() => {}) as any
|
||||
|
||||
setSend(send: SendType) {
|
||||
this.modelingSend = send
|
||||
this.throttledModelingSend = throttle(send, 100)
|
||||
}
|
||||
|
||||
overlayTimeout = 0
|
||||
callbacks: (() => SegmentOverlayPayload | null)[] = []
|
||||
|
||||
_overlayCallbacks(callbacks: (() => SegmentOverlayPayload | null)[]) {
|
||||
const segmentOverlayPayload: SegmentOverlayPayload = {
|
||||
type: 'add-many',
|
||||
@ -179,6 +187,7 @@ export class SceneInfra {
|
||||
data: segmentOverlayPayload,
|
||||
})
|
||||
}
|
||||
|
||||
overlayCallbacks(
|
||||
callbacks: (() => SegmentOverlayPayload | null)[],
|
||||
instant = false
|
||||
@ -195,6 +204,7 @@ export class SceneInfra {
|
||||
}
|
||||
|
||||
overlayThrottleMap: { [pathToNodeString: string]: number } = {}
|
||||
|
||||
updateOverlayDetails({
|
||||
handle,
|
||||
group,
|
||||
@ -349,6 +359,7 @@ export class SceneInfra {
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
// Dispose of any other resources like geometries, materials, textures
|
||||
}
|
||||
|
||||
getClientSceneScaleFactor(meshOrGroup: Mesh | Group) {
|
||||
const orthoFactor = orthoScale(this.camControls.camera)
|
||||
const factor =
|
||||
@ -358,6 +369,7 @@ export class SceneInfra {
|
||||
this._baseUnitMultiplier
|
||||
return factor
|
||||
}
|
||||
|
||||
getPlaneIntersectPoint = (): {
|
||||
twoD?: Vector2
|
||||
threeD?: Vector3
|
||||
@ -556,6 +568,7 @@ export class SceneInfra {
|
||||
(a, b) => a.distance - b.distance
|
||||
)
|
||||
}
|
||||
|
||||
updateMouseState(mouseState: MouseState) {
|
||||
if (this.lastMouseState.type === mouseState.type) return
|
||||
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) {
|
||||
|
@ -54,6 +54,7 @@ import {
|
||||
STRAIGHT_SEGMENT,
|
||||
STRAIGHT_SEGMENT_BODY,
|
||||
STRAIGHT_SEGMENT_DASH,
|
||||
STRAIGHT_SEGMENT_SNAP_LINE,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT_BODY,
|
||||
TANGENTIAL_ARC_TO__SEGMENT_DASH,
|
||||
@ -216,7 +217,17 @@ class StraightSegment implements SegmentUtils {
|
||||
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)
|
||||
|
||||
let updateOverlaysCallback = this.update({
|
||||
prevSegment,
|
||||
input,
|
||||
@ -265,20 +276,39 @@ class StraightSegment implements SegmentUtils {
|
||||
isHandlesVisible = !shouldHideHover
|
||||
}
|
||||
|
||||
if (arrowGroup) {
|
||||
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()
|
||||
|
||||
if (arrowGroup) {
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
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)
|
||||
if (extraSegmentGroup) {
|
||||
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 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({
|
||||
arcStartPoint: from,
|
||||
arcEndPoint: to,
|
||||
tanPreviousPoint: previousPoint,
|
||||
tanPreviousPoint: getTanPreviousPoint(prevSegment),
|
||||
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 {
|
||||
init: SegmentUtils['init'] = ({
|
||||
prevSegment,
|
||||
@ -1018,21 +1051,18 @@ class ArcSegment implements SegmentUtils {
|
||||
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
|
||||
@ -1398,7 +1428,7 @@ class ThreePointArcSegment implements SegmentUtils {
|
||||
p3,
|
||||
radius,
|
||||
center,
|
||||
ccw: false,
|
||||
ccw: !isClockwise([p1, p2, p3]),
|
||||
prevSegment,
|
||||
pathToNode,
|
||||
isSelected,
|
||||
@ -1992,12 +2022,10 @@ export function dashedStraight(
|
||||
function createLine({
|
||||
from,
|
||||
to,
|
||||
scale,
|
||||
color,
|
||||
}: {
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
scale: number
|
||||
color: number
|
||||
}): Line {
|
||||
// Implementation for creating a line
|
||||
|
@ -312,7 +312,7 @@ export function getPathToExtrudeForSegmentSelection(
|
||||
export function mutateAstWithTagForSketchSegment(
|
||||
astClone: Node<Program>,
|
||||
pathToSegmentNode: PathToNode
|
||||
): { modifiedAst: Program; tag: string } | Error {
|
||||
): { modifiedAst: Node<Program>; tag: string } | Error {
|
||||
const segmentNode = getNodeFromPath<CallExpression | CallExpressionKw>(
|
||||
astClone,
|
||||
pathToSegmentNode,
|
||||
|
@ -20,10 +20,12 @@ import {
|
||||
} from '@src/lang/constants'
|
||||
import {
|
||||
createArrayExpression,
|
||||
createBinaryExpression,
|
||||
createCallExpression,
|
||||
createCallExpressionStdLibKw,
|
||||
createLabeledArg,
|
||||
createLiteral,
|
||||
createLocalName,
|
||||
createObjectExpression,
|
||||
createPipeExpression,
|
||||
createPipeSubstitution,
|
||||
@ -60,6 +62,7 @@ import type {
|
||||
SingleValueInput,
|
||||
SketchLineHelper,
|
||||
SketchLineHelperKw,
|
||||
addCall,
|
||||
} from '@src/lang/std/stdTypes'
|
||||
import {
|
||||
findKwArg,
|
||||
@ -2324,8 +2327,9 @@ export const circleThreePoint: SketchLineHelperKw = {
|
||||
return finalConstraints
|
||||
},
|
||||
}
|
||||
|
||||
export const angledLine: SketchLineHelperKw = {
|
||||
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
|
||||
add: ({ node, pathToNode, segmentInput, replaceExistingCallback, snaps }) => {
|
||||
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
|
||||
const { from, to } = segmentInput
|
||||
const _node = { ...node }
|
||||
@ -2334,11 +2338,27 @@ export const angledLine: SketchLineHelperKw = {
|
||||
if (err(_node1)) return _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 newLine = createCallExpressionStdLibKw('angledLine', null, [
|
||||
createLabeledArg('angle', newAngleVal),
|
||||
createLabeledArg('length', newLengthVal),
|
||||
createLabeledArg(ARG_ANGLE, newAngleVal),
|
||||
createLabeledArg(ARG_LENGTH, newLengthVal),
|
||||
])
|
||||
|
||||
if (replaceExistingCallback) {
|
||||
@ -2348,7 +2368,12 @@ export const angledLine: SketchLineHelperKw = {
|
||||
type: 'labeledArg',
|
||||
key: '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',
|
||||
@ -3309,6 +3334,7 @@ interface CreateLineFnCallArgs {
|
||||
fnName: ToolTip
|
||||
pathToNode: PathToNode
|
||||
spliceBetween?: boolean
|
||||
snaps?: addCall['snaps']
|
||||
}
|
||||
|
||||
export function addNewSketchLn({
|
||||
@ -3318,6 +3344,7 @@ export function addNewSketchLn({
|
||||
pathToNode,
|
||||
input: segmentInput,
|
||||
spliceBetween = false,
|
||||
snaps,
|
||||
}: CreateLineFnCallArgs):
|
||||
| {
|
||||
modifiedAst: Node<Program>
|
||||
@ -3347,6 +3374,7 @@ export function addNewSketchLn({
|
||||
pathToNode,
|
||||
segmentInput,
|
||||
spliceBetween,
|
||||
snaps,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1538,20 +1538,22 @@ export function removeSingleConstraint({
|
||||
}
|
||||
if (inputToReplace.type === 'arrayItem') {
|
||||
const values = inputs.map((arg) => {
|
||||
const argExpr = arg.overrideExpr ?? arg.expr
|
||||
if (
|
||||
!(
|
||||
(arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') &&
|
||||
arg.index === inputToReplace.index
|
||||
)
|
||||
)
|
||||
return arg.expr
|
||||
const literal = rawArgs.find(
|
||||
return argExpr
|
||||
const rawArg = rawArgs.find(
|
||||
(rawValue) =>
|
||||
(rawValue.type === 'arrayItem' ||
|
||||
rawValue.type === 'arrayOrObjItem') &&
|
||||
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') {
|
||||
return createStdlibCallExpression(
|
||||
@ -1589,6 +1591,7 @@ export function removeSingleConstraint({
|
||||
const objInput: Parameters<typeof createObjectExpression>[0] = {}
|
||||
const kwArgInput: ReturnType<typeof createLabeledArg>[] = []
|
||||
inputs.forEach((currentArg) => {
|
||||
const currentArgExpr = currentArg.overrideExpr ?? currentArg.expr
|
||||
if (
|
||||
// should be one of these, return early to make TS happy.
|
||||
currentArg.type !== 'objectProperty' &&
|
||||
@ -1619,8 +1622,11 @@ export function removeSingleConstraint({
|
||||
if (!arrayInput[currentArg.key]) {
|
||||
arrayInput[currentArg.key] = []
|
||||
}
|
||||
arrayInput[inputToReplace.key][inputToReplace.index] =
|
||||
const rawLiteralArrayInObjectExpr =
|
||||
rawLiteralArrayInObject.overrideExpr ??
|
||||
rawLiteralArrayInObject.expr
|
||||
arrayInput[inputToReplace.key][inputToReplace.index] =
|
||||
rawLiteralArrayInObjectExpr
|
||||
let existingKwgForKey = kwArgInput.find(
|
||||
(kwArg) => kwArg.label.name === currentArg.key
|
||||
)
|
||||
@ -1633,7 +1639,7 @@ export function removeSingleConstraint({
|
||||
}
|
||||
if (existingKwgForKey.arg.type === 'ArrayExpression') {
|
||||
existingKwgForKey.arg.elements[inputToReplace.index] =
|
||||
rawLiteralArrayInObject.expr
|
||||
rawLiteralArrayInObjectExpr
|
||||
}
|
||||
} else if (
|
||||
inputToReplace.type === 'objectProperty' &&
|
||||
@ -1642,10 +1648,11 @@ export function removeSingleConstraint({
|
||||
rawLiteralObjProp?.key === inputToReplace.key &&
|
||||
currentArg.key === inputToReplace.key
|
||||
) {
|
||||
objInput[inputToReplace.key] = rawLiteralObjProp.expr
|
||||
objInput[inputToReplace.key] =
|
||||
rawLiteralObjProp.overrideExpr ?? rawLiteralObjProp.expr
|
||||
} else if (currentArg.type === 'arrayInObject') {
|
||||
if (!arrayInput[currentArg.key]) arrayInput[currentArg.key] = []
|
||||
arrayInput[currentArg.key][currentArg.index] = currentArg.expr
|
||||
arrayInput[currentArg.key][currentArg.index] = currentArgExpr
|
||||
let existingKwgForKey = kwArgInput.find(
|
||||
(kwArg) => kwArg.label.name === currentArg.key
|
||||
)
|
||||
@ -1657,10 +1664,10 @@ export function removeSingleConstraint({
|
||||
kwArgInput.push(existingKwgForKey)
|
||||
}
|
||||
if (existingKwgForKey.arg.type === 'ArrayExpression') {
|
||||
existingKwgForKey.arg.elements[currentArg.index] = currentArg.expr
|
||||
existingKwgForKey.arg.elements[currentArg.index] = currentArgExpr
|
||||
}
|
||||
} else if (currentArg.type === 'objectProperty') {
|
||||
objInput[currentArg.key] = currentArg.expr
|
||||
objInput[currentArg.key] = currentArgExpr
|
||||
}
|
||||
})
|
||||
const createObjParam: Parameters<typeof createObjectExpression>[0] = {}
|
||||
@ -1694,7 +1701,7 @@ export function removeSingleConstraint({
|
||||
|
||||
return createCallWrapper(
|
||||
callExp.node.callee.name.name as any,
|
||||
rawArgs[0].expr,
|
||||
rawArgs[0].overrideExpr ?? rawArgs[0].expr,
|
||||
tag
|
||||
)
|
||||
},
|
||||
|
@ -44,6 +44,7 @@ interface StraightSegmentInput {
|
||||
type: 'straight-segment'
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
snap?: boolean
|
||||
}
|
||||
|
||||
/** Inputs for arcs, excluding tangentialArc for reasons explain in the
|
||||
@ -92,6 +93,12 @@ export interface addCall extends ModifyAstBase {
|
||||
) => CreatedSketchExprResult | Error
|
||||
referencedSegment?: Path
|
||||
spliceBetween?: boolean
|
||||
snaps?: {
|
||||
previousArcTag?: string
|
||||
negativeTangentDirection: boolean
|
||||
xAxis?: boolean
|
||||
yAxis?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
interface updateArgs extends ModifyAstBase {
|
||||
@ -121,18 +128,21 @@ export interface SingleValueInput<T> {
|
||||
type: 'singleValue'
|
||||
argType: LineInputsType
|
||||
expr: T
|
||||
overrideExpr?: Node<Expr>
|
||||
}
|
||||
export interface ArrayItemInput<T> {
|
||||
type: 'arrayItem'
|
||||
index: 0 | 1
|
||||
argType: LineInputsType
|
||||
expr: T
|
||||
overrideExpr?: Node<Expr>
|
||||
}
|
||||
export interface ObjectPropertyInput<T> {
|
||||
type: 'objectProperty'
|
||||
key: InputArgKeys
|
||||
argType: LineInputsType
|
||||
expr: T
|
||||
overrideExpr?: Node<Expr>
|
||||
}
|
||||
|
||||
interface ArrayOrObjItemInput<T> {
|
||||
@ -141,6 +151,7 @@ interface ArrayOrObjItemInput<T> {
|
||||
index: 0 | 1
|
||||
argType: LineInputsType
|
||||
expr: T
|
||||
overrideExpr?: Node<Expr>
|
||||
}
|
||||
|
||||
interface ArrayInObject<T> {
|
||||
@ -149,6 +160,7 @@ interface ArrayInObject<T> {
|
||||
argType: LineInputsType
|
||||
index: 0 | 1
|
||||
expr: T
|
||||
overrideExpr?: Node<Expr>
|
||||
}
|
||||
|
||||
interface LabeledArg<T> {
|
||||
@ -156,6 +168,7 @@ interface LabeledArg<T> {
|
||||
key: InputArgKeys
|
||||
argType: LineInputsType
|
||||
expr: T
|
||||
overrideExpr?: Node<Expr>
|
||||
}
|
||||
|
||||
type _InputArg<T> =
|
||||
|
@ -23,6 +23,10 @@ export function isArray(val: any): val is unknown[] {
|
||||
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.
|
||||
*
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { Coords2d } from '@src/lang/std/sketch'
|
||||
import { isPointsCCW } from '@src/lang/wasm'
|
||||
import { initPromise } from '@src/lang/wasmUtils'
|
||||
import { closestPointOnRay } from '@src/lib/utils2d'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
@ -20,3 +21,72 @@ describe('test isPointsCW', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
@ -17,3 +17,38 @@ export function getTangentPointFromPreviousArc(
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -866,10 +866,11 @@ export const modelingMachine = setup({
|
||||
if (twoD) {
|
||||
sceneInfra.modelingSend({
|
||||
type: 'click in scene',
|
||||
data: sceneEntitiesManager.getSnappedDragPoint({
|
||||
intersection2d: twoD,
|
||||
intersects: args.intersects,
|
||||
}).snappedPoint,
|
||||
data: sceneEntitiesManager.getSnappedDragPoint(
|
||||
twoD,
|
||||
args.intersects,
|
||||
args.mouseEvent
|
||||
).snappedPoint,
|
||||
})
|
||||
} else {
|
||||
console.error('No intersection point found')
|
||||
@ -1238,10 +1239,11 @@ export const modelingMachine = setup({
|
||||
if (!intersectionPoint?.twoD) return
|
||||
if (!context.sketchDetails) return
|
||||
const { snappedPoint, isSnapped } =
|
||||
sceneEntitiesManager.getSnappedDragPoint({
|
||||
intersection2d: intersectionPoint.twoD,
|
||||
intersects: args.intersects,
|
||||
})
|
||||
sceneEntitiesManager.getSnappedDragPoint(
|
||||
intersectionPoint.twoD,
|
||||
args.intersects,
|
||||
args.mouseEvent
|
||||
)
|
||||
if (isSnapped) {
|
||||
sceneEntitiesManager.positionDraftPoint({
|
||||
snappedPoint: new Vector2(...snappedPoint),
|
||||
|
@ -82,16 +82,14 @@ const SignIn = () => {
|
||||
style={
|
||||
isDesktop()
|
||||
? ({
|
||||
'-webkit-app-region': 'drag',
|
||||
WebkitAppRegion: 'drag',
|
||||
} as CSSProperties)
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
isDesktop()
|
||||
? ({ '-webkit-app-region': 'no-drag' } as CSSProperties)
|
||||
: {}
|
||||
isDesktop() ? ({ WebkitAppRegion: 'no-drag' } as CSSProperties) : {}
|
||||
}
|
||||
className="body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto"
|
||||
>
|
||||
|
Reference in New Issue
Block a user