Skip to content

#11766 New angle dimension, PointsAtAngle#11977

Open
andrewvarga wants to merge 121 commits into
mainfrom
andrewvarga/11766/points-at-angle
Open

#11766 New angle dimension, PointsAtAngle#11977
andrewvarga wants to merge 121 commits into
mainfrom
andrewvarga/11766/points-at-angle

Conversation

@andrewvarga

@andrewvarga andrewvarga commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Implements the new PointsAtAngle constraint for lines with the new UX where users can define the "sector" to apply the angle constraint to:

Screenshare.-.2026-06-26.9_44_15.AM.mp4

There is a long conversation on Slack with @davreev about implementation details if you're interested in the details.

1. Old syntax uses the existing lines ezpz constraint:

P&C: not available. KCL still supported to not break KCL.

@settings(kclVersion = 2.0)

sketch001 = sketch(on = XZ) {
  line1 = line(start = [var -5.01mm, var -1.28mm], end = [var 5.83mm, var 4.75mm])
  line2 = line(start = [var 0.47mm, var -4.27mm], end = [var 0.72mm, var 6.5mm])
  angle([line1, line2]) == 59.58deg
}

2. New syntax uses the new PointsAtAngle constraint.

P+C: select dimension tool, and click on the 2 lines, or select 2 lines, then click on the dimension tool.
It does matter which part of the line you click on!

@settings(kclVersion = 2.0)

sketch001 = sketch(on = XZ) {
  line1 = line(start = [var -5.01mm, var -1.28mm], end = [var 5.83mm, var 4.75mm])
  line2 = line(start = [var 0.47mm, var -4.27mm], end = [var 0.72mm, var 6.5mm])
  angleDimension(
    lines = [line1, line2],
    sector = 1,
    inverse = false,
    labelPosition = [2.24mm, 5.44mm],
  ) == 59.58deg
}

KCL syntax

  • sector means which quadrant to use out of the 4 defined by the 2 intersecting lines. Each line has a forward and a reverse direction, we take the arc sweep in CCW:
sector 1: line1 forward -> line2 forward
sector 2: line2 forward -> line1 reverse
sector 3: line1 reverse -> line2 reverse
sector 4: line2 reverse -> line1 forward
  • inverse is true to use the “inverse angle” of the specified sector so the angle which completes the full circle (inverse of the arc)
    Alternative names would be outer / opposite / reflex.

  • labelPosition is the rendered position for the angle label (the numeric value displayed in the sketch rendering).

Implementation details

angleRadius(
  lines = [line2, line1],
  sector = 1,
  inverse = false,
  labelPosition = [-2.29mm, -0.34mm],
) == 60.28deg

PointsAtAngle(vertex, ray0, ray1, angle_kind) requires a vertex point and 2 rays between which we want to constraint the angle for, so this is what we do:

  • Compute the infinite line intersection point which becomes the vertex, create a new hidden solver point in ezpz
  • Pick either the start or the end point of each line, depending on where they are: if a point is on the intersection, we pick the other point to avoid a zero length ray so we pick the point furthest from the intersection.
  • Keep track if the picked points is on the line's forward or reverse direction (forward is start point -> end point)
  • Compute the requested sector rays from sector and inverse
  • Remap the angle + sector to the equivalent angle between the picked line points. ***see example below
  • Constrain the hidden vertex onto both infinite lines
  • Emit PointsAtAngle:
Constraint::PointsAtAngle(
  vertex,
  representative_point_on_line0,
  representative_point_on_line1,
  remapped_angle_kind,
)

*** Examples for mapping:

settings(kclVersion = 2.0)

sketch001 = sketch(on = XY) {
  // line1 = right
  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
  // line2 = up/right, 60deg from line1
  line2 = line(start = [var 0mm, var 0mm], end = [var 5mm, var 8.66mm])

  angleRadius(
    lines = [line1, line2],
    sector = 3,
    labelPosition = [-2.55mm, -1.77mm]
  ) == 60deg
}

Sector 3 is where we take the reversed direction for both lines from the intersection vertex. The lines are not going into the negative direction because they both start at the origin, but in this case this is the same as enforcing 60deg between the two forward directions:

PointsAtAngle(vertex, line1.end, line2.end, 60deg)

In other cases, the angle needs to be mapped too, for example:

angle(lines = [line1, line2], sector = 2, labelPosition = [-2.21mm, 2.1mm]) == 180 - 60deg

Sector 2 means line2 forwardline1 reverse.

Now we can only use the lines’ end points, meaning the forward directions of the lines, because the start points are exactly at the intersection vertex. In general, we take the furthest endpoints from the intersection vertex.

So we want line2 forwardline1 reverse to be 180 - 60 = 120deg. Since we can only use the forward directions, we map that to line1 forwardline2 forward = 60deg:

PointsAtAngle(vertex, line1.end, line2.end, 60deg)

Both examples map to the same underlying constraint, which makes sense because they use the same lines and the same angle between them, just defined via a different sector.

@andrewvarga andrewvarga requested review from a team as code owners June 4, 2026 14:04
@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
modeling-app Ready Ready Preview, Comment Jul 15, 2026 6:29am

Request Review

@andrewvarga andrewvarga marked this pull request as draft June 4, 2026 14:04
@codspeed-hq

codspeed-hq Bot commented Jun 4, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 126 untouched benchmarks
⏩ 151 skipped benchmarks1


Comparing andrewvarga/11766/points-at-angle (36f40c8) with main (3be31df)2

Open in CodSpeed

Footnotes

  1. 151 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (93a716c) during the generation of this report, so 3be31df was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@franknoirot

Copy link
Copy Markdown
Contributor

Just one bit of UX feedback: selecting segments before equipping the Dimension tool should not fully auto-submit anymore, but instead halt the user on the "sector" selection step, I believe.

Screenshare.-.2026-06-10.1_38_14.PM-compressed.mp4

@andrewvarga

Copy link
Copy Markdown
Contributor Author

Andrew and I caught up, and I realised there were a couple of things I’d completely blanked on in terms of what we should consider before we hit merge on this.

One is checking Zookeeper’s performance. Whenever we make an API change to KCL, there’s a chance Zookeeper’s performance regresses because it might get confused by the new function etc. That’s the case here since there’s a new preferred function.

I left a comment about marking the old one as deprecated, and that might be even more pragmatic in the context of trying to get Zookeeper to write the right thing.

The easiest way to test Zookeeper’s performance is to copy all the docs over to the correct place in the text-to-CAD repo, put that up as a PR, and it will auto-deploy a new endpoint with that version of Zookeeper. You can then use the dev URL with a query param for the PR number.

Then, in the Vercel preview for the ZDS change, you can use the URL override to point Zookeeper at your custom preview URL. That gives you a new app with the new Zookeeper, so you can sanity check performance before we merge. Since it’s all preview URLs, you can also send it around with instructions so other people can test as well.

The other related thing is that it might be worth looking into whether we can have a lint, and maybe an auto-fix lint, that changes the old function into the new one. This may not be possible because the old function is pretty loosey-goosey and the new one is exact but it’s worth at least considering.

There’s a Zookeeper factor here too. Having the lint may help Zookeeper, but maybe Zookeeper doesn’t need it. That’s why I think we should test performance first. If Zookeeper is fine, maybe we don’t need to bother. But if performance regresses, then we should look at what levers we can pull to get it back up. Maybe that’s docs wording, or maybe the lint rule helps steer Zookeeper onto the right path.

As a tip, I mentioned a “local full stack development” doc I created which covers how to copy various OpenAPI specs and other artifacts between repos. That’s useful here for copying docs over into ZK:

https://wiki.hawk-dinosaur.ts.net/en/eng/full-stack-local-development

Or clone:

https://github.com/KittyCAD/wiki

and point your agent at:

eng/full-stack-local-development.md

@Irev-Dev I've updated this PR since our last discussion with these items:

Comment thread src/lang/langHelpers.ts
Comment on lines +203 to +209
discovered_findings = discovered_findings.filter(
(lint) =>
lint.finding.code !== 'Z0007' ||
legacyAngleRefactorMetadata.some((metadata) =>
sourceRangesEqual(metadata.sourceRange, lint.pos)
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does filtering it out mean that it doesn't show up at all if there's an execution error that stops the metadata from being populated?

Or is it more the refactor button doesn't show up in that case but there's still the lint warning?

I ask because might still be good to show there's a problem in cases where we can't actually do the refactor, but even if you prefer the other maybe having a test for this?

Not sure how? maybe a kcl file with a syntax error after the old angle constraint?

@Irev-Dev

Copy link
Copy Markdown
Contributor
Screenshare.-.2026-07-15.3_39_46.PM.mp4

@Irev-Dev

Copy link
Copy Markdown
Contributor

I fixed the issue I left the video about

The sketch-solve update path can change or restore KCL without doing a fresh full execute, so the old lint/code-action state could get stale. That meant the refactor button could either disappear after checkpoint undo, or hang around after label edits but do nothing because it was still pointing at an old source range.

Now when we sync a sketch-solve outcome, we clear stale diagnostics and rebuild the lint diagnostics from the synced source plus the refactor metadata from Rust.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants