Add 3 point arc (#5722)
* bare minimum * start of segment util added * remove redundant handle * some probably buggy handling of arc handles, can fix later * probably bug implementation of update args, but data flow through is mostly there can fix bugs after * fix update for arc * fix math for center handle * fix up length indicator * tweak math * stub out xState logic for arc * more progress on adding point and click, implemented more of sketchLineHelper for arc * small unrelated tweak * fix up draft arc bugs * fix arc last click * fix draft segment animation and add comment * add draft point snapping for arcs * add helper stuff to arc * clone arc point and click as base for arc-three-point * rust change for arc three point * can draw three point arc * make arcTo editable * can add new three point arc, so long as it continues existing profile * get overlays working * make snap to for continuing profile work for three point arcs * add draft animation * tangent issue fix * action rename * tmp test fix up * fix silly bug * fix couple problems causing tests to fail * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * fix up * add delet segment test for new segments * update docs * draft segments should look right * add test for dragging new segment handles * arc tools can be chained now * make three point arc can start a new profile (not only extend existing paths) * add test for equiping and unequiping the tool plus drawing with it * fix console noise * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * clean up * update rust/docs * put toolbar mode check into fixture * do thing for lee * use TEST_COLORSs * fix colors * don't await file write * remove commented code * remove unneeded template strings * power to **2 * remove magic numbers * more string templates * some odd bits of clean up * arc should be enable in dev * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * add new simulation test * fix test code from kwark migration * issues Frank found * fix deleting half complete ark * fix * small fix on dele index * tsc post main merge * fix up snaping to profile start * add cross hari for three point arc * block snapping if it's the only segment * add tests for canceling arcTo halfway through --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -168,7 +168,7 @@ export function Toolbar({
|
||||
|
||||
return (
|
||||
<menu
|
||||
data-currentMode={currentMode}
|
||||
data-current-mode={currentMode}
|
||||
className="max-w-full whitespace-nowrap rounded-b px-2 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 relative border border-chalkboard-30 dark:border-chalkboard-80 border-t-0 shadow-sm"
|
||||
>
|
||||
<ul
|
||||
|
@ -147,7 +147,8 @@ export const ClientSideScene = ({
|
||||
state.matches({ Sketch: 'Tangential arc to' }) ||
|
||||
state.matches({ Sketch: 'Rectangle tool' }) ||
|
||||
state.matches({ Sketch: 'Circle tool' }) ||
|
||||
state.matches({ Sketch: 'Circle three point tool' })
|
||||
state.matches({ Sketch: 'Circle three point tool' }) ||
|
||||
state.matches({ Sketch: 'Arc three point tool' })
|
||||
) {
|
||||
cursor = 'crosshair'
|
||||
} else {
|
||||
@ -490,14 +491,19 @@ const SegmentMenu = ({
|
||||
verticalPosition === 'top' ? 'bottom-full' : 'top-full'
|
||||
} z-10 w-36 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50`}
|
||||
>
|
||||
<button
|
||||
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
|
||||
onClick={() => {
|
||||
send({ type: 'Constrain remove constraints', data: pathToNode })
|
||||
}}
|
||||
>
|
||||
Remove constraints
|
||||
</button>
|
||||
{stdLibFnName !== 'arcTo' && (
|
||||
<button
|
||||
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
|
||||
onClick={() => {
|
||||
send({
|
||||
type: 'Constrain remove constraints',
|
||||
data: pathToNode,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Remove constraints
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="!border-transparent rounded-sm text-left p-1 text-nowrap"
|
||||
title={
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -49,7 +49,19 @@ import {
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT_BODY,
|
||||
TANGENTIAL_ARC_TO__SEGMENT_DASH,
|
||||
ARC_SEGMENT,
|
||||
ARC_SEGMENT_BODY,
|
||||
ARC_SEGMENT_DASH,
|
||||
ARC_ANGLE_END,
|
||||
getParentGroup,
|
||||
ARC_CENTER_TO_FROM,
|
||||
ARC_CENTER_TO_TO,
|
||||
ARC_ANGLE_REFERENCE_LINE,
|
||||
THREE_POINT_ARC_SEGMENT,
|
||||
THREE_POINT_ARC_SEGMENT_BODY,
|
||||
THREE_POINT_ARC_SEGMENT_DASH,
|
||||
THREE_POINT_ARC_HANDLE2,
|
||||
THREE_POINT_ARC_HANDLE3,
|
||||
} from './sceneEntities'
|
||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
import {
|
||||
@ -61,7 +73,7 @@ import {
|
||||
SEGMENT_LENGTH_LABEL_TEXT,
|
||||
} from './sceneInfra'
|
||||
import { Themes, getThemeColorForThreeJs } from 'lib/theme'
|
||||
import { normaliseAngle, roundOff } from 'lib/utils'
|
||||
import { isClockwise, normaliseAngle, roundOff } from 'lib/utils'
|
||||
import {
|
||||
SegmentOverlay,
|
||||
SegmentOverlayPayload,
|
||||
@ -74,6 +86,7 @@ import { Selections } from 'lib/selections'
|
||||
import { calculate_circle_from_3_points } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
const ANGLE_INDICATOR_RADIUS = 30 // in px
|
||||
interface CreateSegmentArgs {
|
||||
input: SegmentInputs
|
||||
prevSegment: Sketch['paths'][number]
|
||||
@ -412,14 +425,28 @@ class TangentialArcToSegment implements SegmentUtils {
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
|
||||
const previousPoint =
|
||||
prevSegment?.type === 'TangentialArcTo'
|
||||
? getTangentPointFromPreviousArc(
|
||||
prevSegment.center,
|
||||
prevSegment.ccw,
|
||||
prevSegment.to
|
||||
)
|
||||
: prevSegment.from
|
||||
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,
|
||||
@ -591,7 +618,6 @@ class CircleSegment implements SegmentUtils {
|
||||
}
|
||||
const { from, center, radius } = input
|
||||
group.userData.from = from
|
||||
// group.userData.to = to
|
||||
group.userData.center = center
|
||||
group.userData.radius = radius
|
||||
group.userData.prevSegment = prevSegment
|
||||
@ -635,8 +661,7 @@ class CircleSegment implements SegmentUtils {
|
||||
}
|
||||
|
||||
if (radiusLengthIndicator) {
|
||||
// The radius indicator is placed at the midpoint of the radius,
|
||||
// at a 45 degree CCW angle from the positive X-axis
|
||||
// The radius indicator is placed halfway between the center and the start angle point
|
||||
const indicatorPoint = {
|
||||
x: center[0] + (Math.cos(Math.PI / 4) * radius) / 2,
|
||||
y: center[1] + (Math.sin(Math.PI / 4) * radius) / 2,
|
||||
@ -648,6 +673,8 @@ class CircleSegment implements SegmentUtils {
|
||||
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
||||
label.innerText = `${roundOff(radius)}`
|
||||
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
||||
|
||||
// Calculate the angle for the label
|
||||
const isPlaneBackFace = center[0] > indicatorPoint.x
|
||||
label.style.setProperty(
|
||||
'--degree',
|
||||
@ -925,6 +952,585 @@ class CircleThreePointSegment implements SegmentUtils {
|
||||
}
|
||||
}
|
||||
|
||||
class ArcSegment implements SegmentUtils {
|
||||
init: SegmentUtils['init'] = ({
|
||||
prevSegment,
|
||||
input,
|
||||
id,
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
theme,
|
||||
isSelected,
|
||||
sceneInfra,
|
||||
}) => {
|
||||
if (input.type !== 'arc-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { from, to, center, radius, ccw } = input
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
|
||||
|
||||
const group = new Group()
|
||||
const geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw,
|
||||
isDashed: isDraftSegment,
|
||||
scale,
|
||||
})
|
||||
const mat = new MeshBasicMaterial({ color })
|
||||
const arcMesh = new Mesh(geometry, mat)
|
||||
const meshType = isDraftSegment ? ARC_SEGMENT_DASH : ARC_SEGMENT_BODY
|
||||
|
||||
// Create handles for the arc
|
||||
|
||||
const endAngleHandle = createArrowhead(scale, theme, color)
|
||||
endAngleHandle.name = ARC_ANGLE_END
|
||||
endAngleHandle.userData.type = ARC_ANGLE_END
|
||||
|
||||
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
|
||||
|
||||
// A radius indicator that appears from the center to the perimeter
|
||||
const radiusIndicatorGroup = createLengthIndicator({
|
||||
from: center,
|
||||
to: from,
|
||||
scale,
|
||||
})
|
||||
|
||||
const grey = 0xaaaaaa
|
||||
|
||||
// Create a line from the center to the 'to' point
|
||||
const centerToFromLine = createLine({
|
||||
from: center,
|
||||
to: from,
|
||||
scale,
|
||||
color: grey, // Light gray color for the line
|
||||
})
|
||||
centerToFromLine.name = ARC_CENTER_TO_FROM
|
||||
const centerToToLine = createLine({
|
||||
from: center,
|
||||
to,
|
||||
scale,
|
||||
color: grey, // Light gray color for the line
|
||||
})
|
||||
centerToToLine.name = ARC_CENTER_TO_TO
|
||||
const angleReferenceLine = createLine({
|
||||
from: [center[0] + (ANGLE_INDICATOR_RADIUS - 2) * scale, center[1]],
|
||||
to: [center[0] + (ANGLE_INDICATOR_RADIUS + 2) * scale, center[1]],
|
||||
scale,
|
||||
color: grey, // Light gray color for the line
|
||||
})
|
||||
angleReferenceLine.name = ARC_ANGLE_REFERENCE_LINE
|
||||
|
||||
// Create a curved line with an arrow to indicate the angle
|
||||
const angleIndicator = createAngleIndicator({
|
||||
center,
|
||||
radius: ANGLE_INDICATOR_RADIUS, // Half the radius for the indicator
|
||||
startAngle: 0,
|
||||
endAngle,
|
||||
scale,
|
||||
color: grey, // Red color for the angle indicator
|
||||
}) as Line
|
||||
angleIndicator.name = 'angleIndicator'
|
||||
|
||||
// Create a new angle indicator for the end angle
|
||||
const endAngleIndicator = createAngleIndicator({
|
||||
center,
|
||||
radius: ANGLE_INDICATOR_RADIUS, // Half the radius for the indicator
|
||||
startAngle: 0,
|
||||
endAngle: (endAngle * Math.PI) / 180,
|
||||
scale,
|
||||
color: grey, // Green color for the end angle indicator
|
||||
}) as Line
|
||||
endAngleIndicator.name = 'endAngleIndicator'
|
||||
|
||||
// Create a length indicator for the end angle
|
||||
const endAngleLengthIndicator = createLengthIndicator({
|
||||
from: center,
|
||||
to: [
|
||||
center[0] + Math.cos(endAngle) * radius,
|
||||
center[1] + Math.sin(endAngle) * radius,
|
||||
],
|
||||
scale,
|
||||
})
|
||||
endAngleLengthIndicator.name = 'endAngleLengthIndicator'
|
||||
|
||||
arcMesh.userData.type = meshType
|
||||
arcMesh.name = meshType
|
||||
group.userData = {
|
||||
type: ARC_SEGMENT,
|
||||
draft: isDraftSegment,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
radius,
|
||||
center,
|
||||
ccw,
|
||||
prevSegment,
|
||||
pathToNode,
|
||||
isSelected,
|
||||
baseColor,
|
||||
}
|
||||
group.name = ARC_SEGMENT
|
||||
|
||||
group.add(
|
||||
arcMesh,
|
||||
endAngleHandle,
|
||||
circleCenterGroup,
|
||||
radiusIndicatorGroup,
|
||||
centerToFromLine,
|
||||
centerToToLine,
|
||||
angleReferenceLine,
|
||||
angleIndicator,
|
||||
endAngleIndicator,
|
||||
endAngleLengthIndicator
|
||||
)
|
||||
const updateOverlaysCallback = this.update({
|
||||
prevSegment,
|
||||
input,
|
||||
group,
|
||||
scale,
|
||||
sceneInfra,
|
||||
})
|
||||
if (err(updateOverlaysCallback)) return updateOverlaysCallback
|
||||
|
||||
return {
|
||||
group,
|
||||
updateOverlaysCallback,
|
||||
}
|
||||
}
|
||||
|
||||
update: SegmentUtils['update'] = ({
|
||||
prevSegment,
|
||||
input,
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
}) => {
|
||||
if (input.type !== 'arc-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { from, to, center, radius, ccw } = input
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
group.userData.center = center
|
||||
group.userData.radius = radius
|
||||
group.userData.ccw = ccw
|
||||
group.userData.prevSegment = prevSegment
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
|
||||
|
||||
// Normalize the angle to -180 to 180 degrees
|
||||
// const normalizedStartAngle = ((startAngle * 180 / Math.PI) + 180) % 360 - 180
|
||||
const normalizedStartAngle = normaliseAngle((startAngle * 180) / Math.PI)
|
||||
const normalizedEndAngle = (((endAngle * 180) / Math.PI + 180) % 360) - 180
|
||||
|
||||
const endAngleHandle = group.getObjectByName(ARC_ANGLE_END) as Group
|
||||
const radiusLengthIndicator = group.getObjectByName(
|
||||
SEGMENT_LENGTH_LABEL
|
||||
) as Group
|
||||
const circleCenterHandle = group.getObjectByName(
|
||||
CIRCLE_CENTER_HANDLE
|
||||
) as Group
|
||||
|
||||
// Calculate arc length
|
||||
let arcAngle = endAngle - startAngle
|
||||
if (ccw && arcAngle > 0) arcAngle = arcAngle - 2 * Math.PI
|
||||
if (!ccw && arcAngle < 0) arcAngle = arcAngle + 2 * Math.PI
|
||||
|
||||
const arcLength = Math.abs(arcAngle) * radius
|
||||
const pxLength = arcLength / scale
|
||||
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
|
||||
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
|
||||
|
||||
const hoveredParent =
|
||||
sceneInfra.hoveredObject &&
|
||||
getParentGroup(sceneInfra.hoveredObject, [ARC_SEGMENT])
|
||||
let isHandlesVisible = !shouldHideIdle
|
||||
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
|
||||
isHandlesVisible = !shouldHideHover
|
||||
}
|
||||
|
||||
if (endAngleHandle) {
|
||||
endAngleHandle.position.set(to[0], to[1], 0)
|
||||
|
||||
const tangentAngle = endAngle + (Math.PI / 2) * (ccw ? 1 : -1)
|
||||
endAngleHandle.quaternion.setFromUnitVectors(
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(Math.cos(tangentAngle), Math.sin(tangentAngle), 0)
|
||||
)
|
||||
endAngleHandle.scale.set(scale, scale, scale)
|
||||
endAngleHandle.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
if (radiusLengthIndicator) {
|
||||
// The radius indicator is placed halfway between the center and the start angle point
|
||||
const indicatorPoint = {
|
||||
x: center[0] + (Math.cos(startAngle) * radius) / 2,
|
||||
y: center[1] + (Math.sin(startAngle) * radius) / 2,
|
||||
}
|
||||
const labelWrapper = radiusLengthIndicator.getObjectByName(
|
||||
SEGMENT_LENGTH_LABEL_TEXT
|
||||
) as CSS2DObject
|
||||
const labelWrapperElem = labelWrapper.element as HTMLDivElement
|
||||
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
||||
label.innerText = `R:${roundOff(radius)}${'\n'}A:${roundOff(
|
||||
roundOff((startAngle * 180) / Math.PI)
|
||||
)}`
|
||||
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
||||
|
||||
// Calculate the angle for the label
|
||||
label.style.setProperty('--degree', `-${startAngle}rad`)
|
||||
label.style.setProperty('--x', `0px`)
|
||||
label.style.setProperty('--y', `0px`)
|
||||
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
|
||||
radiusLengthIndicator.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
if (circleCenterHandle) {
|
||||
circleCenterHandle.position.set(center[0], center[1], 0)
|
||||
circleCenterHandle.scale.set(scale, scale, scale)
|
||||
circleCenterHandle.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const arcSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === ARC_SEGMENT_BODY
|
||||
) as Mesh
|
||||
|
||||
if (arcSegmentBody) {
|
||||
const newGeo = createArcGeometry({
|
||||
radius,
|
||||
center,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw,
|
||||
scale,
|
||||
})
|
||||
arcSegmentBody.geometry = newGeo
|
||||
}
|
||||
|
||||
const arcSegmentBodyDashed = group.getObjectByName(ARC_SEGMENT_DASH)
|
||||
if (arcSegmentBodyDashed instanceof Mesh) {
|
||||
arcSegmentBodyDashed.geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw,
|
||||
isDashed: true,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
|
||||
const centerToFromLine = group.getObjectByName(ARC_CENTER_TO_FROM) as Line
|
||||
if (centerToFromLine) {
|
||||
updateLine(centerToFromLine, { from: center, to: from, scale })
|
||||
centerToFromLine.visible = isHandlesVisible
|
||||
}
|
||||
const centerToToLine = group.getObjectByName(ARC_CENTER_TO_TO) as Line
|
||||
if (centerToToLine) {
|
||||
updateLine(centerToToLine, { from: center, to, scale })
|
||||
centerToToLine.visible = isHandlesVisible
|
||||
}
|
||||
const angleReferenceLine = group.getObjectByName(
|
||||
ARC_ANGLE_REFERENCE_LINE
|
||||
) as Line
|
||||
if (angleReferenceLine) {
|
||||
updateLine(angleReferenceLine, {
|
||||
from: center,
|
||||
to: [center[0] + 34 * scale, center[1]],
|
||||
scale,
|
||||
})
|
||||
angleReferenceLine.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const angleIndicator = group.getObjectByName('angleIndicator') as Line
|
||||
if (angleIndicator) {
|
||||
updateAngleIndicator(angleIndicator, {
|
||||
center,
|
||||
radiusPx: ANGLE_INDICATOR_RADIUS - 10,
|
||||
startAngle: 0,
|
||||
endAngle: (normalizedStartAngle * Math.PI) / 180,
|
||||
scale,
|
||||
})
|
||||
angleIndicator.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const endAngleIndicator = group.getObjectByName('endAngleIndicator') as Line
|
||||
if (endAngleIndicator) {
|
||||
updateAngleIndicator(endAngleIndicator, {
|
||||
center,
|
||||
radiusPx: ANGLE_INDICATOR_RADIUS,
|
||||
startAngle: 0,
|
||||
endAngle: (normalizedEndAngle * Math.PI) / 180,
|
||||
scale,
|
||||
})
|
||||
endAngleIndicator.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const endAngleLengthIndicator = group.getObjectByName(
|
||||
'endAngleLengthIndicator'
|
||||
) as Group
|
||||
if (endAngleLengthIndicator) {
|
||||
const labelWrapper = endAngleLengthIndicator.getObjectByName(
|
||||
SEGMENT_LENGTH_LABEL_TEXT
|
||||
) as CSS2DObject
|
||||
const labelWrapperElem = labelWrapper.element as HTMLDivElement
|
||||
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
||||
label.innerText = `A:${roundOff(normalizedEndAngle)}`
|
||||
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
||||
|
||||
// Position the label
|
||||
const indicatorPoint = {
|
||||
x: center[0] + (Math.cos(endAngle) * radius) / 2,
|
||||
y: center[1] + (Math.sin(endAngle) * radius) / 2,
|
||||
}
|
||||
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
|
||||
endAngleLengthIndicator.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
return () =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
handle: endAngleHandle,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from,
|
||||
to,
|
||||
angle: endAngle + (Math.PI / 2) * (ccw ? 1 : -1),
|
||||
hasThreeDotMenu: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class ThreePointArcSegment implements SegmentUtils {
|
||||
init: SegmentUtils['init'] = ({
|
||||
input,
|
||||
id,
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
theme,
|
||||
isSelected = false,
|
||||
sceneInfra,
|
||||
prevSegment,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { p1, p2, p3 } = input
|
||||
const { center_x, center_y, radius } = calculate_circle_from_3_points(
|
||||
p1[0],
|
||||
p1[1],
|
||||
p2[0],
|
||||
p2[1],
|
||||
p3[0],
|
||||
p3[1]
|
||||
)
|
||||
const center: [number, number] = [center_x, center_y]
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
||||
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
|
||||
|
||||
const group = new Group()
|
||||
const geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw: !isClockwise([p1, p2, p3]),
|
||||
isDashed: isDraftSegment,
|
||||
scale,
|
||||
})
|
||||
const mat = new MeshBasicMaterial({ color })
|
||||
const arcMesh = new Mesh(geometry, mat)
|
||||
const meshType = isDraftSegment
|
||||
? THREE_POINT_ARC_SEGMENT_DASH
|
||||
: THREE_POINT_ARC_SEGMENT_BODY
|
||||
|
||||
// Create handles for p2 and p3 using createCircleThreePointHandle
|
||||
const p2Handle = createCircleThreePointHandle(
|
||||
scale,
|
||||
theme,
|
||||
THREE_POINT_ARC_HANDLE2,
|
||||
color
|
||||
)
|
||||
p2Handle.position.set(p2[0], p2[1], 0)
|
||||
|
||||
const p3Handle = createCircleThreePointHandle(
|
||||
scale,
|
||||
theme,
|
||||
THREE_POINT_ARC_HANDLE3,
|
||||
color
|
||||
)
|
||||
p3Handle.position.set(p3[0], p3[1], 0)
|
||||
|
||||
arcMesh.userData.type = meshType
|
||||
arcMesh.name = meshType
|
||||
group.userData = {
|
||||
type: THREE_POINT_ARC_SEGMENT,
|
||||
draft: isDraftSegment,
|
||||
id,
|
||||
from: p1,
|
||||
to: p3,
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
radius,
|
||||
center,
|
||||
ccw: false,
|
||||
prevSegment,
|
||||
pathToNode,
|
||||
isSelected,
|
||||
baseColor,
|
||||
}
|
||||
group.name = THREE_POINT_ARC_SEGMENT
|
||||
|
||||
group.add(arcMesh, p2Handle, p3Handle)
|
||||
const updateOverlaysCallback = this.update({
|
||||
prevSegment,
|
||||
input,
|
||||
group,
|
||||
scale,
|
||||
sceneInfra,
|
||||
})
|
||||
if (err(updateOverlaysCallback)) return updateOverlaysCallback
|
||||
|
||||
return {
|
||||
group,
|
||||
updateOverlaysCallback,
|
||||
}
|
||||
}
|
||||
|
||||
update: SegmentUtils['update'] = ({
|
||||
prevSegment,
|
||||
input,
|
||||
group,
|
||||
scale = 1,
|
||||
sceneInfra,
|
||||
}) => {
|
||||
if (input.type !== 'circle-three-point-segment') {
|
||||
return new Error('Invalid segment type')
|
||||
}
|
||||
const { p1, p2, p3 } = input
|
||||
const { center_x, center_y, radius } = calculate_circle_from_3_points(
|
||||
p1[0],
|
||||
p1[1],
|
||||
p2[0],
|
||||
p2[1],
|
||||
p3[0],
|
||||
p3[1]
|
||||
)
|
||||
const center: [number, number] = [center_x, center_y]
|
||||
group.userData.from = p1
|
||||
group.userData.to = p3
|
||||
group.userData.p1 = p1
|
||||
group.userData.p2 = p2
|
||||
group.userData.p3 = p3
|
||||
group.userData.center = center
|
||||
group.userData.radius = radius
|
||||
group.userData.prevSegment = prevSegment
|
||||
|
||||
// Calculate start and end angles
|
||||
const startAngle = Math.atan2(p1[1] - center[1], p1[0] - center[0])
|
||||
const endAngle = Math.atan2(p3[1] - center[1], p3[0] - center[0])
|
||||
|
||||
const p2Handle = group.getObjectByName(THREE_POINT_ARC_HANDLE2) as Group
|
||||
const p3Handle = group.getObjectByName(THREE_POINT_ARC_HANDLE3) as Group
|
||||
|
||||
const arcSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === THREE_POINT_ARC_SEGMENT_BODY
|
||||
) as Mesh
|
||||
|
||||
if (arcSegmentBody) {
|
||||
const newGeo = createArcGeometry({
|
||||
radius,
|
||||
center,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw: !isClockwise([p1, p2, p3]),
|
||||
scale,
|
||||
})
|
||||
arcSegmentBody.geometry = newGeo
|
||||
}
|
||||
|
||||
const arcSegmentBodyDashed = group.getObjectByName(
|
||||
THREE_POINT_ARC_SEGMENT_DASH
|
||||
)
|
||||
if (arcSegmentBodyDashed instanceof Mesh) {
|
||||
arcSegmentBodyDashed.geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
ccw: !isClockwise([p1, p2, p3]),
|
||||
isDashed: true,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
|
||||
if (p2Handle) {
|
||||
p2Handle.position.set(p2[0], p2[1], 0)
|
||||
p2Handle.scale.set(scale, scale, scale)
|
||||
p2Handle.visible = true
|
||||
}
|
||||
|
||||
if (p3Handle) {
|
||||
p3Handle.position.set(p3[0], p3[1], 0)
|
||||
p3Handle.scale.set(scale, scale, scale)
|
||||
p3Handle.visible = true
|
||||
}
|
||||
|
||||
return () => {
|
||||
const overlays: SegmentOverlays = {}
|
||||
const overlayDetails = [p2Handle, p3Handle].map((handle, index) =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
handle: handle,
|
||||
group,
|
||||
isHandlesVisible: true,
|
||||
from: p1,
|
||||
to: p3,
|
||||
angle: endAngle + Math.PI / 2,
|
||||
hasThreeDotMenu: true,
|
||||
})
|
||||
)
|
||||
const segmentOverlays: SegmentOverlay[] = []
|
||||
|
||||
overlayDetails.forEach((payload, index) => {
|
||||
if (payload?.type === 'set-one') {
|
||||
overlays[payload.pathToNodeString] = payload.seg
|
||||
// Add filterValue: 'interior' for p2 and 'end' for p3
|
||||
segmentOverlays.push({
|
||||
...payload.seg[0],
|
||||
filterValue: index === 0 ? 'interior' : 'end',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const segmentOverlayPayload: SegmentOverlayPayload = {
|
||||
type: 'set-one',
|
||||
pathToNodeString:
|
||||
overlayDetails[0]?.type === 'set-one'
|
||||
? overlayDetails[0].pathToNodeString
|
||||
: '',
|
||||
seg: segmentOverlays,
|
||||
}
|
||||
return segmentOverlayPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createProfileStartHandle({
|
||||
from,
|
||||
isDraft = false,
|
||||
@ -1010,7 +1616,7 @@ function createCircleCenterHandle(
|
||||
function createCircleThreePointHandle(
|
||||
scale = 1,
|
||||
theme: Themes,
|
||||
name: `circle-three-point-handle${'1' | '2' | '3'}`,
|
||||
name: string,
|
||||
color?: number
|
||||
): Group {
|
||||
const circleCenterGroup = new Group()
|
||||
@ -1249,9 +1855,11 @@ export function createArcGeometry({
|
||||
)
|
||||
)
|
||||
const remainingArcGeometry = new ExtrudeGeometry(shape, {
|
||||
steps: 50,
|
||||
steps: 1,
|
||||
bevelEnabled: false,
|
||||
extrudePath: remainingArcPath,
|
||||
extrudePath: new CatmullRomCurve3(
|
||||
remainingArcPoints.map((p) => new Vector3(p.x, p.y, 0))
|
||||
),
|
||||
})
|
||||
dashGeometries.push(remainingArcGeometry)
|
||||
}
|
||||
@ -1351,10 +1959,111 @@ export function dashedStraight(
|
||||
geo.userData.type = 'dashed'
|
||||
return geo
|
||||
}
|
||||
function createLine({
|
||||
from,
|
||||
to,
|
||||
scale,
|
||||
color,
|
||||
}: {
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
scale: number
|
||||
color: number
|
||||
}): Line {
|
||||
// Implementation for creating a line
|
||||
const lineGeometry = new BufferGeometry().setFromPoints([
|
||||
new Vector3(from[0], from[1], 0),
|
||||
new Vector3(to[0], to[1], 0),
|
||||
])
|
||||
const lineMaterial = new LineBasicMaterial({ color })
|
||||
return new Line(lineGeometry, lineMaterial)
|
||||
}
|
||||
|
||||
function updateLine(
|
||||
line: Line,
|
||||
{
|
||||
from,
|
||||
to,
|
||||
scale,
|
||||
}: { from: [number, number]; to: [number, number]; scale: number }
|
||||
) {
|
||||
// Implementation for updating a line
|
||||
const points = [
|
||||
new Vector3(from[0], from[1], 0),
|
||||
new Vector3(to[0], to[1], 0),
|
||||
]
|
||||
line.geometry.setFromPoints(points)
|
||||
}
|
||||
|
||||
function createAngleIndicator({
|
||||
center,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
scale,
|
||||
color,
|
||||
}: {
|
||||
center: [number, number]
|
||||
radius: number
|
||||
startAngle: number
|
||||
endAngle: number
|
||||
scale: number
|
||||
color: number
|
||||
}): Line {
|
||||
// Implementation for creating an angle indicator
|
||||
const curve = new EllipseCurve(
|
||||
center[0],
|
||||
center[1],
|
||||
radius,
|
||||
radius,
|
||||
startAngle,
|
||||
endAngle,
|
||||
false,
|
||||
0
|
||||
)
|
||||
const points = curve.getPoints(50)
|
||||
const geometry = new BufferGeometry().setFromPoints(points)
|
||||
const material = new LineBasicMaterial({ color })
|
||||
return new Line(geometry, material)
|
||||
}
|
||||
|
||||
function updateAngleIndicator(
|
||||
angleIndicator: Line,
|
||||
{
|
||||
center,
|
||||
radiusPx,
|
||||
startAngle,
|
||||
endAngle,
|
||||
scale,
|
||||
}: {
|
||||
center: [number, number]
|
||||
radiusPx: number
|
||||
startAngle: number
|
||||
endAngle: number
|
||||
scale: number
|
||||
}
|
||||
) {
|
||||
// Implementation for updating an angle indicator
|
||||
|
||||
const curve = new EllipseCurve(
|
||||
center[0],
|
||||
center[1],
|
||||
radiusPx * scale,
|
||||
radiusPx * scale,
|
||||
startAngle,
|
||||
endAngle,
|
||||
endAngle < startAngle,
|
||||
0
|
||||
)
|
||||
const points = curve.getPoints(50)
|
||||
angleIndicator.geometry.setFromPoints(points)
|
||||
}
|
||||
|
||||
export const segmentUtils = {
|
||||
straight: new StraightSegment(),
|
||||
tangentialArcTo: new TangentialArcToSegment(),
|
||||
circle: new CircleSegment(),
|
||||
circleThreePoint: new CircleThreePointSegment(),
|
||||
arc: new ArcSegment(),
|
||||
threePointArc: new ThreePointArcSegment(),
|
||||
} as const
|
||||
|
@ -70,6 +70,7 @@ import {
|
||||
import {
|
||||
KclValue,
|
||||
PathToNode,
|
||||
PipeExpression,
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
parse,
|
||||
@ -1497,6 +1498,48 @@ export const ModelingMachineProvider = ({
|
||||
return result
|
||||
}
|
||||
),
|
||||
'set-up-draft-arc-three-point': fromPromise(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
return reject('No sketch details or data')
|
||||
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
const result = await sceneEntitiesManager.setupDraftArcThreePoint(
|
||||
sketchDetails.sketchEntryNodePath,
|
||||
sketchDetails.sketchNodePaths,
|
||||
sketchDetails.planeNodePath,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin,
|
||||
data
|
||||
)
|
||||
if (err(result)) return reject(result)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||
|
||||
return result
|
||||
}
|
||||
),
|
||||
'set-up-draft-arc': fromPromise(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
return reject('No sketch details or data')
|
||||
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
const result = await sceneEntitiesManager.setupDraftArc(
|
||||
sketchDetails.sketchEntryNodePath,
|
||||
sketchDetails.sketchNodePaths,
|
||||
sketchDetails.planeNodePath,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin,
|
||||
data
|
||||
)
|
||||
if (err(result)) return reject(result)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||
|
||||
return result
|
||||
}
|
||||
),
|
||||
'setup-client-side-sketch-segments': fromPromise(
|
||||
async ({ input: { sketchDetails, selectionRanges } }) => {
|
||||
if (!sketchDetails) return
|
||||
@ -1568,24 +1611,53 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
|
||||
const indexToDelete = sketchDetails?.expressionIndexToDelete || -1
|
||||
let isLastInPipeThreePointArc = false
|
||||
if (indexToDelete >= 0) {
|
||||
// this is the expression that was added when as sketch tool was used but not completed
|
||||
// i.e first click for the center of the circle, but not the second click for the radius
|
||||
// we added a circle to editor, but they bailed out early so we should remove it
|
||||
moddedAst.body.splice(indexToDelete, 1)
|
||||
// make sure the deleted expression is removed from the sketchNodePaths
|
||||
updatedSketchNodePaths = updatedSketchNodePaths.filter(
|
||||
(path) => path[1][0] !== indexToDelete
|
||||
|
||||
const pipe = getNodeFromPath<PipeExpression>(
|
||||
moddedAst,
|
||||
pathToProfile,
|
||||
'PipeExpression'
|
||||
)
|
||||
// if the deleted expression was the entryNodePath, we should just make it the first sketchNodePath
|
||||
// as a safe default
|
||||
pathToProfile =
|
||||
pathToProfile[1][0] !== indexToDelete
|
||||
? pathToProfile
|
||||
: updatedSketchNodePaths[0]
|
||||
if (err(pipe)) {
|
||||
isLastInPipeThreePointArc = false
|
||||
} else {
|
||||
const lastInPipe = pipe?.node?.body?.[pipe.node.body.length - 1]
|
||||
if (
|
||||
lastInPipe &&
|
||||
Number(pathToProfile[1][0]) === indexToDelete &&
|
||||
lastInPipe.type === 'CallExpression' &&
|
||||
lastInPipe.callee.type === 'Identifier' &&
|
||||
lastInPipe.callee.name === 'arcTo'
|
||||
) {
|
||||
isLastInPipeThreePointArc = true
|
||||
pipe.node.body = pipe.node.body.slice(0, -1)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLastInPipeThreePointArc) {
|
||||
moddedAst.body.splice(indexToDelete, 1)
|
||||
// make sure the deleted expression is removed from the sketchNodePaths
|
||||
updatedSketchNodePaths = updatedSketchNodePaths.filter(
|
||||
(path) => path[1][0] !== indexToDelete
|
||||
)
|
||||
// if the deleted expression was the entryNodePath, we should just make it the first sketchNodePath
|
||||
// as a safe default
|
||||
pathToProfile =
|
||||
pathToProfile[1][0] !== indexToDelete
|
||||
? pathToProfile
|
||||
: updatedSketchNodePaths[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (doesNeedSplitting || indexToDelete >= 0) {
|
||||
if (
|
||||
doesNeedSplitting ||
|
||||
indexToDelete >= 0 ||
|
||||
isLastInPipeThreePointArc
|
||||
) {
|
||||
await kclManager.executeAstMock(moddedAst)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(moddedAst)
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ export type ToolTip =
|
||||
| 'tangentialArcTo'
|
||||
| 'circle'
|
||||
| 'circleThreePoint'
|
||||
| 'arcTo'
|
||||
| 'arc'
|
||||
|
||||
export const toolTips: Array<ToolTip> = [
|
||||
'line',
|
||||
@ -44,6 +46,8 @@ export const toolTips: Array<ToolTip> = [
|
||||
'angledLineThatIntersects',
|
||||
'tangentialArcTo',
|
||||
'circleThreePoint',
|
||||
'arc',
|
||||
'arcTo',
|
||||
]
|
||||
|
||||
export async function executeAst({
|
||||
|
@ -48,6 +48,7 @@ import {
|
||||
RawArgs,
|
||||
CreatedSketchExprResult,
|
||||
SketchLineHelperKw,
|
||||
InputArgKeys,
|
||||
} from 'lang/std/stdTypes'
|
||||
|
||||
import {
|
||||
@ -984,7 +985,10 @@ export const tangentialArcTo: SketchLineHelper = {
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode,
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
['body', 'PipeExpression'],
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
@ -1227,6 +1231,658 @@ export const circle: SketchLineHelperKw = {
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
export const arc: SketchLineHelper = {
|
||||
add: ({
|
||||
node,
|
||||
variables,
|
||||
pathToNode,
|
||||
segmentInput,
|
||||
replaceExistingCallback,
|
||||
spliceBetween,
|
||||
}) => {
|
||||
if (segmentInput.type !== 'arc-segment') return ARC_SEGMENT_ERR
|
||||
const { center, radius, from, to } = segmentInput
|
||||
const _node = { ...node }
|
||||
|
||||
const nodeMeta = getNodeFromPath<PipeExpression | CallExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
const { node: pipe } = nodeMeta
|
||||
|
||||
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
const { node: varDec } = nodeMeta2
|
||||
|
||||
// Calculate start angle (from center to 'from' point)
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
|
||||
// Calculate end angle (from center to 'to' point)
|
||||
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
|
||||
|
||||
// Create literals for the angles (convert to degrees)
|
||||
const startAngleDegrees = (startAngle * 180) / Math.PI
|
||||
const endAngleDegrees = (endAngle * 180) / Math.PI
|
||||
|
||||
// Create the arc call expression
|
||||
const arcObj = createObjectExpression({
|
||||
radius: createLiteral(roundOff(radius)),
|
||||
angleStart: createLiteral(roundOff(startAngleDegrees)),
|
||||
angleEnd: createLiteral(roundOff(endAngleDegrees)),
|
||||
})
|
||||
|
||||
const newArc = createCallExpression('arc', [
|
||||
arcObj,
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
|
||||
if (
|
||||
spliceBetween &&
|
||||
!replaceExistingCallback &&
|
||||
pipe.type === 'PipeExpression'
|
||||
) {
|
||||
const pathToNodeIndex = pathToNode.findIndex(
|
||||
(x) => x[1] === 'PipeExpression'
|
||||
)
|
||||
const pipeIndex = pathToNode[pathToNodeIndex + 1][0]
|
||||
if (typeof pipeIndex === 'undefined' || typeof pipeIndex === 'string') {
|
||||
return new Error('pipeIndex is undefined')
|
||||
}
|
||||
pipe.body = [
|
||||
...pipe.body.slice(0, pipeIndex),
|
||||
newArc,
|
||||
...pipe.body.slice(pipeIndex),
|
||||
]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
if (replaceExistingCallback && pipe.type !== 'CallExpression') {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const result = replaceExistingCallback([
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'center',
|
||||
argType: 'xRelative',
|
||||
expr: createLiteral(roundOff(center[0])),
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'center',
|
||||
argType: 'yRelative',
|
||||
expr: createLiteral(roundOff(center[1])),
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'radius',
|
||||
argType: 'radius',
|
||||
expr: createLiteral(roundOff(radius)),
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'angle',
|
||||
argType: 'angle',
|
||||
expr: createLiteral(roundOff(startAngleDegrees)),
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'angle',
|
||||
argType: 'angle',
|
||||
expr: createLiteral(roundOff(endAngleDegrees)),
|
||||
},
|
||||
])
|
||||
if (err(result)) return result
|
||||
const { callExp, valueUsedInTransform } = result
|
||||
pipe.body[callIndex] = callExp
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
valueUsedInTransform,
|
||||
}
|
||||
}
|
||||
|
||||
if (pipe.type === 'PipeExpression') {
|
||||
pipe.body = [...pipe.body, newArc]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
}
|
||||
} else {
|
||||
varDec.init = createPipeExpression([varDec.init, newArc])
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, input }) => {
|
||||
if (input.type !== 'arc-segment') return ARC_SEGMENT_ERR
|
||||
const { center, radius, from, to } = input
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: callExpression, shallowPath } = nodeMeta
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
|
||||
if (firstArg.type !== 'ObjectExpression') {
|
||||
return new Error('Expected object expression as first argument')
|
||||
}
|
||||
|
||||
// Calculate start angle (from center to 'from' point)
|
||||
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
||||
|
||||
// Calculate end angle (from center to 'to' point)
|
||||
const endAngle = Math.atan2(to[1] - center[1], to[0] - center[0])
|
||||
|
||||
// Create literals for the angles (convert to degrees)
|
||||
const startAngleDegrees = (startAngle * 180) / Math.PI
|
||||
const endAngleDegrees = (endAngle * 180) / Math.PI
|
||||
|
||||
// Update radius
|
||||
const newRadius = createLiteral(roundOff(radius))
|
||||
mutateObjExpProp(firstArg, newRadius, 'radius')
|
||||
|
||||
// Update angleStart
|
||||
const newAngleStart = createLiteral(roundOff(startAngleDegrees))
|
||||
mutateObjExpProp(firstArg, newAngleStart, 'angleStart')
|
||||
|
||||
// Update angleEnd
|
||||
const newAngleEnd = createLiteral(roundOff(endAngleDegrees))
|
||||
mutateObjExpProp(firstArg, newAngleEnd, 'angleEnd')
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: shallowPath,
|
||||
}
|
||||
},
|
||||
getTag: getTag(),
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, code, pathToNode, filterValue) => {
|
||||
// TODO this isn't quiet right, the filter value needs to be added to group the radius and start angle together
|
||||
// with the end angle by itself,
|
||||
// also both angles are just called angle, which is not correct
|
||||
if (callExp.type !== 'CallExpression') return []
|
||||
const args = callExp.arguments
|
||||
if (args.length < 1) return []
|
||||
|
||||
const firstArg = args[0]
|
||||
if (firstArg.type !== 'ObjectExpression') return []
|
||||
|
||||
// Find radius, angleStart, and angleEnd properties
|
||||
const radiusProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'radius'
|
||||
)
|
||||
|
||||
const angleStartProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'angleStart'
|
||||
)
|
||||
|
||||
const angleEndProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'angleEnd'
|
||||
)
|
||||
|
||||
if (!radiusProp || !angleStartProp || !angleEndProp) return []
|
||||
|
||||
const pathToFirstArg: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
const pathToRadiusProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(radiusProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToAngleStartProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(angleStartProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToAngleEndProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(angleEndProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToRadiusValue: PathToNode = [
|
||||
...pathToRadiusProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const pathToAngleStartValue: PathToNode = [
|
||||
...pathToAngleStartProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const pathToAngleEndValue: PathToNode = [
|
||||
...pathToAngleEndProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const constraints: ConstrainInfo[] = [
|
||||
constrainInfo(
|
||||
'radius',
|
||||
isNotLiteralArrayOrStatic(radiusProp.value),
|
||||
code.slice(radiusProp.value.start, radiusProp.value.end),
|
||||
'arc',
|
||||
'radius',
|
||||
topLevelRange(radiusProp.value.start, radiusProp.value.end),
|
||||
pathToRadiusValue
|
||||
),
|
||||
constrainInfo(
|
||||
'angle',
|
||||
isNotLiteralArrayOrStatic(angleStartProp.value),
|
||||
code.slice(angleStartProp.value.start, angleStartProp.value.end),
|
||||
'arc',
|
||||
'angleStart',
|
||||
topLevelRange(angleStartProp.value.start, angleStartProp.value.end),
|
||||
pathToAngleStartValue
|
||||
),
|
||||
constrainInfo(
|
||||
'angle',
|
||||
isNotLiteralArrayOrStatic(angleEndProp.value),
|
||||
code.slice(angleEndProp.value.start, angleEndProp.value.end),
|
||||
'arc',
|
||||
'angleEnd',
|
||||
topLevelRange(angleEndProp.value.start, angleEndProp.value.end),
|
||||
pathToAngleEndValue
|
||||
),
|
||||
]
|
||||
|
||||
return constraints
|
||||
},
|
||||
}
|
||||
export const arcTo: SketchLineHelper = {
|
||||
add: ({
|
||||
node,
|
||||
variables,
|
||||
pathToNode,
|
||||
segmentInput,
|
||||
replaceExistingCallback,
|
||||
spliceBetween,
|
||||
}) => {
|
||||
if (segmentInput.type !== 'circle-three-point-segment')
|
||||
return ARC_SEGMENT_ERR
|
||||
|
||||
const { p2, p3 } = segmentInput
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: pipe } = nodeMeta
|
||||
|
||||
// p1 is the start point (from the previous segment)
|
||||
// p2 is the interior point
|
||||
// p3 is the end point
|
||||
const interior = createArrayExpression([
|
||||
createLiteral(roundOff(p2[0], 2)),
|
||||
createLiteral(roundOff(p2[1], 2)),
|
||||
])
|
||||
|
||||
const end = createArrayExpression([
|
||||
createLiteral(roundOff(p3[0], 2)),
|
||||
createLiteral(roundOff(p3[1], 2)),
|
||||
])
|
||||
|
||||
if (replaceExistingCallback) {
|
||||
const result = replaceExistingCallback([
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'interior' as InputArgKeys,
|
||||
argType: 'xAbsolute',
|
||||
expr: createLiteral(0) as any, // This is a workaround, the actual value will be set later
|
||||
},
|
||||
{
|
||||
type: 'objectProperty',
|
||||
key: 'end' as InputArgKeys,
|
||||
argType: 'yAbsolute',
|
||||
expr: createLiteral(0) as any, // This is a workaround, the actual value will be set later
|
||||
},
|
||||
])
|
||||
if (err(result)) return result
|
||||
const { callExp, valueUsedInTransform } = result
|
||||
|
||||
// Now manually update the object properties
|
||||
if (
|
||||
callExp.type === 'CallExpression' &&
|
||||
callExp.arguments[0]?.type === 'ObjectExpression'
|
||||
) {
|
||||
const objExp = callExp.arguments[0]
|
||||
const interiorProp = objExp.properties.find(
|
||||
(p) =>
|
||||
p.type === 'ObjectProperty' &&
|
||||
p.key.type === 'Identifier' &&
|
||||
p.key.name === 'interior'
|
||||
)
|
||||
const endProp = objExp.properties.find(
|
||||
(p) =>
|
||||
p.type === 'ObjectProperty' &&
|
||||
p.key.type === 'Identifier' &&
|
||||
p.key.name === 'end'
|
||||
)
|
||||
|
||||
if (interiorProp && interiorProp.type === 'ObjectProperty') {
|
||||
interiorProp.value = interior
|
||||
}
|
||||
if (endProp && endProp.type === 'ObjectProperty') {
|
||||
endProp.value = end
|
||||
}
|
||||
}
|
||||
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
pipe.body[callIndex] = callExp
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
valueUsedInTransform,
|
||||
}
|
||||
}
|
||||
|
||||
const objExp = createObjectExpression({
|
||||
interior,
|
||||
end,
|
||||
})
|
||||
|
||||
const newLine = createCallExpression('arcTo', [
|
||||
objExp,
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
|
||||
if (spliceBetween) {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
pipe.body.splice(callIndex + 1, 0, newLine)
|
||||
} else if (pipe.type === 'PipeExpression') {
|
||||
pipe.body.push(newLine)
|
||||
} else {
|
||||
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(nodeMeta2)) return nodeMeta2
|
||||
const { node: varDec } = nodeMeta2
|
||||
varDec.init = createPipeExpression([varDec.init, newLine])
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([key, _]) => key === 'init') + 1
|
||||
),
|
||||
['body', 'PipeExpression'],
|
||||
[1, 'CallExpression'],
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [
|
||||
...pathToNode.slice(
|
||||
0,
|
||||
pathToNode.findIndex(([_, type]) => type === 'PipeExpression') + 1
|
||||
),
|
||||
[pipe.body.length - 1, 'CallExpression'],
|
||||
],
|
||||
}
|
||||
},
|
||||
updateArgs: ({ node, pathToNode, input }) => {
|
||||
if (input.type !== 'circle-three-point-segment') return ARC_SEGMENT_ERR
|
||||
|
||||
const { p1, p2, p3 } = input
|
||||
const _node = { ...node }
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
// Update the first argument which should be an object with interior and end properties
|
||||
const firstArg = callExpression.arguments?.[0]
|
||||
if (!firstArg) return new Error('Missing first argument in arcTo')
|
||||
|
||||
const interiorPoint = createArrayExpression([
|
||||
createLiteral(roundOff(p2[0], 2)),
|
||||
createLiteral(roundOff(p2[1], 2)),
|
||||
])
|
||||
|
||||
const endPoint = createArrayExpression([
|
||||
createLiteral(roundOff(p3[0], 2)),
|
||||
createLiteral(roundOff(p3[1], 2)),
|
||||
])
|
||||
|
||||
mutateObjExpProp(firstArg, interiorPoint, 'interior')
|
||||
mutateObjExpProp(firstArg, endPoint, 'end')
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
},
|
||||
getTag: getTag(),
|
||||
addTag: addTag(),
|
||||
getConstraintInfo: (callExp, code, pathToNode, filterValue) => {
|
||||
if (callExp.type !== 'CallExpression') return []
|
||||
const args = callExp.arguments
|
||||
if (args.length < 1) return []
|
||||
|
||||
const firstArg = args[0]
|
||||
if (firstArg.type !== 'ObjectExpression') return []
|
||||
|
||||
// Find interior and end properties
|
||||
const interiorProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'interior'
|
||||
)
|
||||
|
||||
const endProp = firstArg.properties.find(
|
||||
(prop) =>
|
||||
prop.type === 'ObjectProperty' &&
|
||||
prop.key.type === 'Identifier' &&
|
||||
prop.key.name === 'end'
|
||||
)
|
||||
|
||||
if (!interiorProp || !endProp) return []
|
||||
if (
|
||||
interiorProp.value.type !== 'ArrayExpression' ||
|
||||
endProp.value.type !== 'ArrayExpression'
|
||||
)
|
||||
return []
|
||||
|
||||
const interiorArr = interiorProp.value
|
||||
const endArr = endProp.value
|
||||
|
||||
if (interiorArr.elements.length < 2 || endArr.elements.length < 2) return []
|
||||
|
||||
const pathToFirstArg: PathToNode = [
|
||||
...pathToNode,
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
const pathToInteriorProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(interiorProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToEndProp: PathToNode = [
|
||||
...pathToFirstArg,
|
||||
['properties', 'ObjectExpression'],
|
||||
[firstArg.properties.indexOf(endProp), 'index'],
|
||||
]
|
||||
|
||||
const pathToInteriorValue: PathToNode = [
|
||||
...pathToInteriorProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const pathToEndValue: PathToNode = [
|
||||
...pathToEndProp,
|
||||
['value', 'ObjectProperty'],
|
||||
]
|
||||
|
||||
const pathToInteriorX: PathToNode = [
|
||||
...pathToInteriorValue,
|
||||
['elements', 'ArrayExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
const pathToInteriorY: PathToNode = [
|
||||
...pathToInteriorValue,
|
||||
['elements', 'ArrayExpression'],
|
||||
[1, 'index'],
|
||||
]
|
||||
|
||||
const pathToEndX: PathToNode = [
|
||||
...pathToEndValue,
|
||||
['elements', 'ArrayExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
const pathToEndY: PathToNode = [
|
||||
...pathToEndValue,
|
||||
['elements', 'ArrayExpression'],
|
||||
[1, 'index'],
|
||||
]
|
||||
|
||||
const constraints: (ConstrainInfo & { filterValue: string })[] = [
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(interiorArr.elements[0]),
|
||||
value: code.slice(
|
||||
interiorArr.elements[0].start,
|
||||
interiorArr.elements[0].end
|
||||
),
|
||||
stdLibFnName: 'arcTo',
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
key: 'interior',
|
||||
index: 0,
|
||||
},
|
||||
sourceRange: topLevelRange(
|
||||
interiorArr.elements[0].start,
|
||||
interiorArr.elements[0].end
|
||||
),
|
||||
pathToNode: pathToInteriorX,
|
||||
filterValue: 'interior',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(interiorArr.elements[1]),
|
||||
value: code.slice(
|
||||
interiorArr.elements[1].start,
|
||||
interiorArr.elements[1].end
|
||||
),
|
||||
stdLibFnName: 'arcTo',
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
key: 'interior',
|
||||
index: 1,
|
||||
},
|
||||
sourceRange: topLevelRange(
|
||||
interiorArr.elements[1].start,
|
||||
interiorArr.elements[1].end
|
||||
),
|
||||
pathToNode: pathToInteriorY,
|
||||
filterValue: 'interior',
|
||||
},
|
||||
{
|
||||
type: 'xAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(endArr.elements[0]),
|
||||
value: code.slice(endArr.elements[0].start, endArr.elements[0].end),
|
||||
stdLibFnName: 'arcTo',
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
key: 'end',
|
||||
index: 0,
|
||||
},
|
||||
sourceRange: topLevelRange(
|
||||
endArr.elements[0].start,
|
||||
endArr.elements[0].end
|
||||
),
|
||||
pathToNode: pathToEndX,
|
||||
filterValue: 'end',
|
||||
},
|
||||
{
|
||||
type: 'yAbsolute',
|
||||
isConstrained: isNotLiteralArrayOrStatic(endArr.elements[1]),
|
||||
value: code.slice(endArr.elements[1].start, endArr.elements[1].end),
|
||||
stdLibFnName: 'arcTo',
|
||||
argPosition: {
|
||||
type: 'arrayInObject',
|
||||
key: 'end',
|
||||
index: 1,
|
||||
},
|
||||
sourceRange: topLevelRange(
|
||||
endArr.elements[1].start,
|
||||
endArr.elements[1].end
|
||||
),
|
||||
pathToNode: pathToEndY,
|
||||
filterValue: 'end',
|
||||
},
|
||||
]
|
||||
|
||||
const finalConstraints: ConstrainInfo[] = []
|
||||
constraints.forEach((constraint) => {
|
||||
if (!filterValue) {
|
||||
finalConstraints.push(constraint)
|
||||
}
|
||||
if (filterValue && constraint.filterValue === filterValue) {
|
||||
finalConstraints.push(constraint)
|
||||
}
|
||||
})
|
||||
return finalConstraints
|
||||
},
|
||||
}
|
||||
|
||||
export const circleThreePoint: SketchLineHelperKw = {
|
||||
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
|
||||
if (segmentInput.type !== 'circle-three-point-segment') {
|
||||
@ -2270,6 +2926,8 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
|
||||
angledLineToY,
|
||||
angledLineThatIntersects,
|
||||
tangentialArcTo,
|
||||
arc,
|
||||
arcTo,
|
||||
} as const
|
||||
|
||||
export const sketchLineHelperMapKw: { [key: string]: SketchLineHelperKw } = {
|
||||
@ -2297,6 +2955,7 @@ export function changeSketchArguments(
|
||||
},
|
||||
input: SegmentInputs
|
||||
): { modifiedAst: Node<Program>; pathToNode: PathToNode } | Error {
|
||||
// TODO/less-than-ideal, this obvious relies on node getting mutated, as changing the following with `_node = structuredClone(node)` breaks the draft line animation.
|
||||
const _node = { ...node }
|
||||
const thePath =
|
||||
sourceRangeOrPath.type === 'sourceRange'
|
||||
@ -2529,7 +3188,7 @@ export function addCallExpressionsToPipe({
|
||||
pathToNode: PathToNode
|
||||
expressions: Node<CallExpression | CallExpressionKw>[]
|
||||
}) {
|
||||
const _node = { ...node }
|
||||
const _node: Node<Program> = structuredClone(node)
|
||||
const pipeExpression = getNodeFromPath<Node<PipeExpression>>(
|
||||
_node,
|
||||
pathToNode,
|
||||
@ -2548,7 +3207,7 @@ export function addCloseToPipe({
|
||||
node,
|
||||
pathToNode,
|
||||
}: {
|
||||
node: Program
|
||||
node: Node<Program>
|
||||
variables: VariableMap
|
||||
pathToNode: PathToNode
|
||||
}) {
|
||||
|
@ -1351,8 +1351,11 @@ export function getRemoveConstraintsTransform(
|
||||
}
|
||||
|
||||
if (
|
||||
sketchFnExp.type === 'CallExpressionKw' &&
|
||||
sketchFnExp.callee.name === 'circleThreePoint'
|
||||
(sketchFnExp.type === 'CallExpressionKw' &&
|
||||
sketchFnExp.callee.name === 'circleThreePoint') ||
|
||||
(sketchFnExp.type === 'CallExpression' &&
|
||||
(sketchFnExp.callee.name === 'arcTo' ||
|
||||
sketchFnExp.callee.name === 'arc'))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@ -1613,6 +1616,9 @@ function getTransformMapPath(
|
||||
if (!toolTips.includes(name)) {
|
||||
return false
|
||||
}
|
||||
if (name === 'arcTo') {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if the function is locked down and so can't be transformed
|
||||
const firstArg = getFirstArg(sketchFnExp)
|
||||
@ -2093,8 +2099,10 @@ export function transformAstSketchLines({
|
||||
center: seg.center,
|
||||
radius: seg.radius,
|
||||
from,
|
||||
to: from, // For a full circle, to is the same as from
|
||||
ccw: true, // Default to counter-clockwise for circles
|
||||
}
|
||||
: seg.type === 'CircleThreePoint'
|
||||
: seg.type === 'CircleThreePoint' || seg.type === 'ArcThreePoint'
|
||||
? {
|
||||
type: 'circle-three-point-segment',
|
||||
p1: seg.p1,
|
||||
|
@ -42,8 +42,10 @@ interface StraightSegmentInput {
|
||||
interface ArcSegmentInput {
|
||||
type: 'arc-segment'
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
center: [number, number]
|
||||
radius: number
|
||||
ccw: boolean
|
||||
}
|
||||
/** Inputs for three point circle */
|
||||
interface CircleThreePointSegmentInput {
|
||||
@ -98,6 +100,9 @@ export type InputArgKeys =
|
||||
| 'p1'
|
||||
| 'p2'
|
||||
| 'p3'
|
||||
| 'end'
|
||||
| 'interior'
|
||||
| `angle${'Start' | 'End'}`
|
||||
export interface SingleValueInput<T> {
|
||||
type: 'singleValue'
|
||||
argType: LineInputsType
|
||||
|
@ -414,10 +414,22 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
},
|
||||
{
|
||||
id: 'three-point-arc',
|
||||
onClick: () => console.error('Three-point arc not yet implemented'),
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingState.matches({ Sketch: 'Arc three point tool' })
|
||||
? 'arcThreePoint'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
icon: 'arc',
|
||||
status: 'unavailable',
|
||||
status: 'available',
|
||||
title: 'Three-point Arc',
|
||||
hotkey: (state) =>
|
||||
state.matches({ Sketch: 'Arc three point tool' })
|
||||
? ['Esc', 'T']
|
||||
: 'T',
|
||||
showTitle: false,
|
||||
description: 'Draw a circular arc defined by three points',
|
||||
links: [
|
||||
@ -426,6 +438,26 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
url: 'https://github.com/KittyCAD/modeling-app/issues/1659',
|
||||
},
|
||||
],
|
||||
isActive: (state) =>
|
||||
state.matches({ Sketch: 'Arc three point tool' }),
|
||||
},
|
||||
{
|
||||
id: 'arc',
|
||||
onClick: ({ modelingState, modelingSend }) =>
|
||||
modelingSend({
|
||||
type: 'change tool',
|
||||
data: {
|
||||
tool: !modelingState.matches({ Sketch: 'Arc tool' })
|
||||
? 'arc'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
icon: 'arc',
|
||||
status: DEV ? 'available' : 'unavailable',
|
||||
title: 'Arc',
|
||||
description: 'Start drawing an arc',
|
||||
links: [],
|
||||
isActive: (state) => state.matches({ Sketch: 'Arc tool' }),
|
||||
},
|
||||
],
|
||||
{
|
||||
@ -473,7 +505,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
tool: !modelingState.matches({
|
||||
Sketch: 'Circle three point tool',
|
||||
})
|
||||
? 'circleThreePointNeo'
|
||||
? 'circleThreePoint'
|
||||
: 'none',
|
||||
},
|
||||
}),
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
onDragNumberCalculation,
|
||||
hasLeadingZero,
|
||||
hasDigitsLeftOfDecimal,
|
||||
isClockwise,
|
||||
} from './utils'
|
||||
import { SourceRange, topLevelRange } from '../lang/wasm'
|
||||
|
||||
@ -1253,3 +1254,56 @@ describe('testing onDragNumberCalculation', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing isClockwise', () => {
|
||||
it('returns for counter clockwise points', () => {
|
||||
// Points in clockwise order (rectangle)
|
||||
const clockwisePoints: [number, number][] = [
|
||||
[0, 0], // bottom-left
|
||||
[10, 0], // bottom-right
|
||||
[10, 10], // top-right
|
||||
[0, 10], // top-left
|
||||
]
|
||||
expect(isClockwise(clockwisePoints)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true for clockwise points', () => {
|
||||
// Points in counter-clockwise order (rectangle)
|
||||
const counterClockwisePoints: [number, number][] = [
|
||||
[0, 0], // bottom-left
|
||||
[0, 10], // top-left
|
||||
[10, 10], // top-right
|
||||
[10, 0], // bottom-right
|
||||
]
|
||||
expect(isClockwise(counterClockwisePoints)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for less than 3 points', () => {
|
||||
expect(
|
||||
isClockwise([
|
||||
[0, 0],
|
||||
[10, 10],
|
||||
])
|
||||
).toBe(false)
|
||||
expect(isClockwise([[0, 0]])).toBe(false)
|
||||
expect(isClockwise([])).toBe(false)
|
||||
})
|
||||
|
||||
it('correctly identifies counter-clockwise triangle', () => {
|
||||
const clockwiseTriangle: [number, number][] = [
|
||||
[0, 0],
|
||||
[10, 0],
|
||||
[5, 10],
|
||||
]
|
||||
expect(isClockwise(clockwiseTriangle)).toBe(false)
|
||||
})
|
||||
|
||||
it('correctly identifies clockwise triangle', () => {
|
||||
const counterClockwiseTriangle: [number, number][] = [
|
||||
[0, 0],
|
||||
[5, 10],
|
||||
[10, 0],
|
||||
]
|
||||
expect(isClockwise(counterClockwiseTriangle)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
@ -387,3 +387,22 @@ export function onMouseDragMakeANewNumber(
|
||||
if (!newVal) return
|
||||
setText(newVal)
|
||||
}
|
||||
|
||||
export function isClockwise(points: [number, number][]): boolean {
|
||||
// Need at least 3 points to determine orientation
|
||||
if (points.length < 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Calculate the sum of (x2 - x1) * (y2 + y1) for all edges
|
||||
// This is the "shoelace formula" for calculating the signed area
|
||||
let sum = 0
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const current = points[i]
|
||||
const next = points[(i + 1) % points.length]
|
||||
sum += (next[0] - current[0]) * (next[1] + current[1])
|
||||
}
|
||||
|
||||
// If sum is positive, the points are in clockwise order
|
||||
return sum > 0
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user