basic circle edit working
This commit is contained in:
@ -62,9 +62,10 @@ import {
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import {
|
||||
circleSegment,
|
||||
createArcGeometry,
|
||||
dashedStraight,
|
||||
profileStart,
|
||||
createProfileStartHandle,
|
||||
straightSegment,
|
||||
tangentialArcToSegment,
|
||||
} from './segments'
|
||||
@ -72,6 +73,7 @@ import {
|
||||
addCallExpressionsToPipe,
|
||||
addCloseToPipe,
|
||||
addNewSketchLn,
|
||||
changeCircleArguments,
|
||||
changeSketchArguments,
|
||||
updateStartProfileAtArgs,
|
||||
} from 'lang/std/sketch'
|
||||
@ -119,6 +121,10 @@ export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
||||
'tangential-arc-to-segment-body-dashed'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
||||
export const CIRCLE_SEGMENT = 'circle-segment'
|
||||
export const CIRCLE_SEGMENT_BODY = 'circle-segment-body'
|
||||
export const CIRCLE_SEGMENT_DASH = 'circle-segment-body-dashed'
|
||||
export const CIRCLE_CENTER_HANDLE = 'circle-center-handle'
|
||||
export const SEGMENT_WIDTH_PX = 1.6
|
||||
export const HIDE_SEGMENT_LENGTH = 75 // in pixels
|
||||
export const HIDE_HOVER_SEGMENT_LENGTH = 60 // in pixels
|
||||
@ -186,6 +192,27 @@ export class SceneEntities {
|
||||
})
|
||||
)
|
||||
}
|
||||
if (
|
||||
segment.userData.from &&
|
||||
segment.userData.to &&
|
||||
segment.userData.center &&
|
||||
segment.userData.radius &&
|
||||
segment.userData.prevSegment &&
|
||||
segment.userData.type === CIRCLE_SEGMENT
|
||||
) {
|
||||
callbacks.push(
|
||||
this.updateCircleSegment({
|
||||
prevSegment: segment.userData.prevSegment,
|
||||
from: segment.userData.from,
|
||||
to: segment.userData.to,
|
||||
center: segment.userData.center,
|
||||
radius: segment.userData.radius,
|
||||
group: segment,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (segment.name === PROFILE_START) {
|
||||
segment.scale.set(factor, factor, factor)
|
||||
}
|
||||
@ -421,19 +448,21 @@ export class SceneEntities {
|
||||
maybeModdedAst,
|
||||
sketchGroup.start.__geoMeta.sourceRange
|
||||
)
|
||||
const _profileStart = profileStart({
|
||||
from: sketchGroup.start.from,
|
||||
id: sketchGroup.start.__geoMeta.id,
|
||||
pathToNode: segPathToNode,
|
||||
scale: factor,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
_profileStart.layers.set(SKETCH_LAYER)
|
||||
_profileStart.traverse((child) => {
|
||||
child.layers.set(SKETCH_LAYER)
|
||||
})
|
||||
group.add(_profileStart)
|
||||
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
||||
if (sketchGroup?.value?.[0].type !== 'Circle') {
|
||||
const _profileStart = createProfileStartHandle({
|
||||
from: sketchGroup.start.from,
|
||||
id: sketchGroup.start.__geoMeta.id,
|
||||
pathToNode: segPathToNode,
|
||||
scale: factor,
|
||||
theme: sceneInfra._theme,
|
||||
})
|
||||
_profileStart.layers.set(SKETCH_LAYER)
|
||||
_profileStart.traverse((child) => {
|
||||
child.layers.set(SKETCH_LAYER)
|
||||
})
|
||||
group.add(_profileStart)
|
||||
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
||||
}
|
||||
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
||||
sketchGroup.value.forEach((segment, index) => {
|
||||
let segPathToNode = getNodePathFromSourceRange(
|
||||
@ -498,6 +527,32 @@ export class SceneEntities {
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
} else if (segment.type === 'Circle') {
|
||||
seg = circleSegment({
|
||||
prevSegment: sketchGroup.value[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
center: segment.center,
|
||||
radius: segment.radius,
|
||||
id: segment.__geoMeta.id,
|
||||
pathToNode: segPathToNode,
|
||||
isDraftSegment,
|
||||
scale: factor,
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
theme: sceneInfra._theme,
|
||||
isSelected,
|
||||
})
|
||||
callbacks.push(
|
||||
this.updateCircleSegment({
|
||||
prevSegment: sketchGroup.value[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
center: segment.center,
|
||||
radius: segment.radius,
|
||||
group: seg,
|
||||
scale: factor,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
seg = straightSegment({
|
||||
from: segment.from,
|
||||
@ -587,6 +642,7 @@ export class SceneEntities {
|
||||
segmentName: 'line' | 'tangentialArcTo' = 'line',
|
||||
shouldTearDown = true
|
||||
) => {
|
||||
// try {
|
||||
const _ast = structuredClone(kclManager.ast)
|
||||
|
||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
||||
@ -602,11 +658,10 @@ export class SceneEntities {
|
||||
kclManager.programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sg)) return sg
|
||||
const lastSeg = sg.value?.slice(-1)[0] || sg.start
|
||||
if (err(sg)) return Promise.reject(sg)
|
||||
const lastSeg = sg?.value?.slice(-1)[0] || sg.start
|
||||
|
||||
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
|
||||
|
||||
const mod = addNewSketchLn({
|
||||
node: _ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
@ -621,7 +676,7 @@ export class SceneEntities {
|
||||
|
||||
const draftExpressionsIndices = { start: index, end: index }
|
||||
|
||||
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
|
||||
// if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
|
||||
sceneInfra.resetMouseListeners()
|
||||
|
||||
const { truncatedAst, programMemoryOverride, sketchGroup } =
|
||||
@ -725,6 +780,9 @@ export class SceneEntities {
|
||||
},
|
||||
...this.mouseEnterLeaveCallbacks(),
|
||||
})
|
||||
// } catch (e) {
|
||||
// console.log('yo wtf', e)
|
||||
// }
|
||||
}
|
||||
setupDraftRectangle = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
@ -746,6 +804,164 @@ export class SceneEntities {
|
||||
const startSketchOn = _node1.node?.declarations
|
||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
||||
|
||||
const sg = sketchGroupFromKclValue(
|
||||
kclManager.programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sg)) return sg
|
||||
const tags: [string, string, string] = [
|
||||
findUniqueName(_ast, 'rectangleSegmentA'),
|
||||
findUniqueName(_ast, 'rectangleSegmentB'),
|
||||
findUniqueName(_ast, 'rectangleSegmentC'),
|
||||
]
|
||||
|
||||
startSketchOn[0].init = createPipeExpression([
|
||||
startSketchOnInit,
|
||||
...getRectangleCallExpressions(rectangleOrigin, tags),
|
||||
])
|
||||
|
||||
let _recastAst = parse(recast(_ast))
|
||||
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
||||
_ast = _recastAst
|
||||
|
||||
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
|
||||
sketchPathToNode,
|
||||
forward,
|
||||
up,
|
||||
position: sketchOrigin,
|
||||
maybeModdedAst: _ast,
|
||||
draftExpressionsIndices: { start: 0, end: 3 },
|
||||
})
|
||||
|
||||
sceneInfra.setCallbacks({
|
||||
onMove: async (args) => {
|
||||
// Update the width and height of the draft rectangle
|
||||
const pathToNodeTwo = structuredClone(sketchPathToNode)
|
||||
pathToNodeTwo[1][0] = 0
|
||||
|
||||
const _node = getNodeFromPath<VariableDeclaration>(
|
||||
truncatedAst,
|
||||
pathToNodeTwo || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return Promise.reject(_node)
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
|
||||
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
|
||||
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||
}
|
||||
|
||||
const { programMemory } = await executeAst({
|
||||
ast: truncatedAst,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
})
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketchGroup = sketchGroupFromKclValue(
|
||||
programMemory.get(variableDeclarationName),
|
||||
variableDeclarationName
|
||||
)
|
||||
if (err(sketchGroup)) return Promise.reject(sketchGroup)
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
this.updateSegment(
|
||||
sketchGroup.start,
|
||||
0,
|
||||
0,
|
||||
_ast,
|
||||
orthoFactor,
|
||||
sketchGroup
|
||||
)
|
||||
sgPaths.forEach((seg, index) =>
|
||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
||||
)
|
||||
},
|
||||
onClick: async (args) => {
|
||||
// Commit the rectangle to the full AST/code and return to sketch.idle
|
||||
const cornerPoint = args.intersectionPoint?.twoD
|
||||
if (!cornerPoint || args.mouseEvent.button !== 0) return
|
||||
|
||||
const x = roundOff((cornerPoint.x || 0) - rectangleOrigin[0])
|
||||
const y = roundOff((cornerPoint.y || 0) - rectangleOrigin[1])
|
||||
|
||||
const _node = getNodeFromPath<VariableDeclaration>(
|
||||
_ast,
|
||||
sketchPathToNode || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node)) return Promise.reject(_node)
|
||||
const sketchInit = _node.node?.declarations?.[0]?.init
|
||||
|
||||
if (sketchInit.type === 'PipeExpression') {
|
||||
updateRectangleSketch(sketchInit, x, y, tags[0])
|
||||
|
||||
let _recastAst = parse(recast(_ast))
|
||||
if (trap(_recastAst)) return Promise.reject(_recastAst)
|
||||
_ast = _recastAst
|
||||
|
||||
// Update the primary AST and unequip the rectangle tool
|
||||
await kclManager.executeAstMock(_ast)
|
||||
sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
||||
|
||||
const { programMemory } = await executeAst({
|
||||
ast: _ast,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
programMemoryOverride,
|
||||
})
|
||||
|
||||
// Prepare to update the THREEjs scene
|
||||
this.sceneProgramMemory = programMemory
|
||||
const sketchGroup = sketchGroupFromKclValue(
|
||||
programMemory.get(
|
||||
variableDeclarationName
|
||||
), variableDeclarationName)
|
||||
if (err(sketchGroup)) return Promise.reject(sketchGroup)
|
||||
const sgPaths = sketchGroup.value
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
// Update the starting segment of the THREEjs scene
|
||||
this.updateSegment(
|
||||
sketchGroup.start,
|
||||
0,
|
||||
0,
|
||||
_ast,
|
||||
orthoFactor,
|
||||
sketchGroup
|
||||
)
|
||||
// Update the rest of the segments of the THREEjs scene
|
||||
sgPaths.forEach((seg, index) =>
|
||||
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
setupDraftCircle = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
forward: [number, number, number],
|
||||
up: [number, number, number],
|
||||
sketchOrigin: [number, number, number],
|
||||
rectangleOrigin: [x: number, y: number]
|
||||
) => {
|
||||
let _ast = structuredClone(kclManager.ast)
|
||||
|
||||
const _node1 = getNodeFromPath<VariableDeclaration>(
|
||||
_ast,
|
||||
sketchPathToNode || [],
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (trap(_node1)) return Promise.reject(_node1)
|
||||
const variableDeclarationName =
|
||||
_node1.node?.declarations?.[0]?.id?.name || ''
|
||||
const startSketchOn = _node1.node?.declarations
|
||||
const startSketchOnInit = startSketchOn?.[0]?.init
|
||||
|
||||
const tags: [string, string, string] = [
|
||||
findUniqueName(_ast, 'rectangleSegmentA'),
|
||||
findUniqueName(_ast, 'rectangleSegmentB'),
|
||||
@ -1047,7 +1263,9 @@ export class SceneEntities {
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
PROFILE_START,
|
||||
CIRCLE_SEGMENT,
|
||||
])
|
||||
const subGroup = getParentGroup(object, [ARROWHEAD, CIRCLE_CENTER_HANDLE])
|
||||
if (!group) return
|
||||
const pathToNode: PathToNode = structuredClone(group.userData.pathToNode)
|
||||
const varDecIndex = pathToNode[1][0]
|
||||
@ -1065,7 +1283,7 @@ export class SceneEntities {
|
||||
group.userData.from[0],
|
||||
group.userData.from[1],
|
||||
]
|
||||
const to: [number, number] = [intersection2d.x, intersection2d.y]
|
||||
const dragTo: [number, number] = [intersection2d.x, intersection2d.y]
|
||||
let modifiedAst = draftInfo ? draftInfo.truncatedAst : { ...kclManager.ast }
|
||||
|
||||
const _node = getNodeFromPath<CallExpression>(
|
||||
@ -1088,16 +1306,42 @@ export class SceneEntities {
|
||||
modded = updateStartProfileAtArgs({
|
||||
node: modifiedAst,
|
||||
pathToNode,
|
||||
to,
|
||||
to: dragTo,
|
||||
from,
|
||||
previousProgramMemory: kclManager.programMemory,
|
||||
})
|
||||
} else if (group.name === CIRCLE_SEGMENT && subGroup?.name === ARROWHEAD) {
|
||||
// is dragging the radius handle
|
||||
modded = changeCircleArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
group.userData.center,
|
||||
Math.sqrt(
|
||||
(group.userData.center[0] - dragTo[0]) ** 2 +
|
||||
(group.userData.center[0] - dragTo[0]) ** 2
|
||||
)
|
||||
)
|
||||
console.log('modded', modded)
|
||||
} else if (
|
||||
group.name === CIRCLE_SEGMENT &&
|
||||
subGroup?.name === CIRCLE_CENTER_HANDLE
|
||||
) {
|
||||
// is dragging the center handle
|
||||
modded = changeCircleArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
dragTo,
|
||||
group.userData.radius
|
||||
)
|
||||
console.log('modded', modded)
|
||||
} else {
|
||||
modded = changeSketchArguments(
|
||||
modifiedAst,
|
||||
kclManager.programMemory,
|
||||
[node.start, node.end],
|
||||
to,
|
||||
dragTo,
|
||||
from
|
||||
)
|
||||
}
|
||||
@ -1216,6 +1460,20 @@ export class SceneEntities {
|
||||
group,
|
||||
scale: factor,
|
||||
})
|
||||
} else if (
|
||||
type === CIRCLE_SEGMENT &&
|
||||
'center' in segment &&
|
||||
'radius' in segment
|
||||
) {
|
||||
return this.updateCircleSegment({
|
||||
prevSegment: sgPaths[index - 1],
|
||||
from: segment.from,
|
||||
to: segment.to,
|
||||
center: segment.center,
|
||||
radius: segment.radius,
|
||||
group,
|
||||
scale: factor,
|
||||
})
|
||||
} else if (type === PROFILE_START) {
|
||||
group.position.set(segment.from[0], segment.from[1], 0)
|
||||
group.scale.set(factor, factor, factor)
|
||||
@ -1241,6 +1499,9 @@ export class SceneEntities {
|
||||
group.userData.prevSegment = prevSegment
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
if (!prevSegment) {
|
||||
console.trace('prevSegment is undefined')
|
||||
}
|
||||
|
||||
const previousPoint =
|
||||
prevSegment?.type === 'TangentialArcTo'
|
||||
@ -1336,6 +1597,111 @@ export class SceneEntities {
|
||||
angle,
|
||||
})
|
||||
}
|
||||
updateCircleSegment({
|
||||
prevSegment,
|
||||
from,
|
||||
to,
|
||||
center,
|
||||
radius,
|
||||
group,
|
||||
scale = 1,
|
||||
}: {
|
||||
prevSegment: SketchGroup['value'][number]
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
center: [number, number]
|
||||
radius: number
|
||||
group: Group
|
||||
scale?: number
|
||||
}): () => SegmentOverlayPayload | null {
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
group.userData.center = center
|
||||
group.userData.radius = radius
|
||||
group.userData.prevSegment = prevSegment
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const circleCenterHandle = group.getObjectByName(
|
||||
CIRCLE_CENTER_HANDLE
|
||||
) as Group
|
||||
|
||||
const pxLength = (2 * radius * Math.PI) / scale
|
||||
const shouldHideIdle = pxLength < HIDE_SEGMENT_LENGTH
|
||||
const shouldHideHover = pxLength < HIDE_HOVER_SEGMENT_LENGTH
|
||||
|
||||
const hoveredParent =
|
||||
sceneInfra.hoveredObject &&
|
||||
getParentGroup(sceneInfra.hoveredObject, [TANGENTIAL_ARC_TO_SEGMENT])
|
||||
let isHandlesVisible = !shouldHideIdle
|
||||
if (hoveredParent && hoveredParent?.uuid === group?.uuid) {
|
||||
isHandlesVisible = !shouldHideHover
|
||||
}
|
||||
|
||||
if (arrowGroup) {
|
||||
arrowGroup.position.set(
|
||||
center[0] + Math.cos(Math.PI / 4) * radius,
|
||||
center[1] + Math.sin(Math.PI / 4) * radius,
|
||||
0
|
||||
)
|
||||
|
||||
const arrowheadAngle = Math.PI / 4
|
||||
arrowGroup.quaternion.setFromUnitVectors(
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
||||
)
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
arrowGroup.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
if (circleCenterHandle) {
|
||||
circleCenterHandle.position.set(center[0], center[1], 0)
|
||||
circleCenterHandle.scale.set(scale, scale, scale)
|
||||
circleCenterHandle.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
const circleSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === CIRCLE_SEGMENT_BODY
|
||||
) as Mesh
|
||||
|
||||
if (circleSegmentBody) {
|
||||
const newGeo = createArcGeometry({
|
||||
radius,
|
||||
center,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
scale,
|
||||
})
|
||||
circleSegmentBody.geometry = newGeo
|
||||
}
|
||||
const circleSegmentBodyDashed = group.children.find(
|
||||
(child) => child.userData.type === CIRCLE_SEGMENT_DASH
|
||||
) as Mesh
|
||||
if (circleSegmentBodyDashed) {
|
||||
// consider throttling the whole updateTangentialArcToSegment
|
||||
// if there are more perf considerations going forward
|
||||
this.throttledUpdateDashedArcGeo({
|
||||
// ...arcInfo,
|
||||
center,
|
||||
radius,
|
||||
ccw: true,
|
||||
startAngle: 0,
|
||||
endAngle: 360,
|
||||
mesh: circleSegmentBodyDashed,
|
||||
isDashed: true,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
const angle = 0
|
||||
return () =>
|
||||
sceneInfra.updateOverlayDetails({
|
||||
arrowGroup,
|
||||
group,
|
||||
isHandlesVisible,
|
||||
from,
|
||||
to,
|
||||
angle,
|
||||
})
|
||||
}
|
||||
throttledUpdateDashedArcGeo = throttle(
|
||||
(
|
||||
args: Parameters<typeof createArcGeometry>[0] & {
|
||||
@ -1470,7 +1836,7 @@ export class SceneEntities {
|
||||
}
|
||||
private _tearDownSketch(
|
||||
callDepth = 0,
|
||||
resolve: (val: unknown) => void,
|
||||
resolve: any,
|
||||
reject: () => void,
|
||||
{ removeAxis = true }: { removeAxis?: boolean }
|
||||
) {
|
||||
@ -1500,7 +1866,7 @@ export class SceneEntities {
|
||||
this._tearDownSketch(callDepth + 1, resolve, reject, { removeAxis })
|
||||
}, delay)
|
||||
} else {
|
||||
reject()
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
sceneInfra.camControls.enableRotate = true
|
||||
@ -1512,7 +1878,7 @@ export class SceneEntities {
|
||||
removeAxis = true,
|
||||
}: {
|
||||
removeAxis?: boolean
|
||||
} = {}) {
|
||||
} = {}): Promise<void | Error> {
|
||||
// I think promisifying this is mostly a side effect of not having
|
||||
// "setupSketch" correctly capture a promise when it's done
|
||||
// so we're effectively waiting for to be finished setting up the scene just to tear it down
|
||||
@ -1578,6 +1944,16 @@ export class SceneEntities {
|
||||
group: parent,
|
||||
scale: factor,
|
||||
})
|
||||
} else if (parent.name === CIRCLE_SEGMENT) {
|
||||
this.updateCircleSegment({
|
||||
prevSegment: parent.userData.prevSegment,
|
||||
from: parent.userData.from,
|
||||
to: parent.userData.to,
|
||||
center: parent.userData.center,
|
||||
radius: parent.userData.radius,
|
||||
group: parent,
|
||||
scale: factor,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -1613,6 +1989,16 @@ export class SceneEntities {
|
||||
group: parent,
|
||||
scale: factor,
|
||||
})
|
||||
} else if (parent.name === CIRCLE_SEGMENT) {
|
||||
this.updateCircleSegment({
|
||||
prevSegment: parent.userData.prevSegment,
|
||||
from: parent.userData.from,
|
||||
to: parent.userData.to,
|
||||
center: parent.userData.center,
|
||||
radius: parent.userData.radius,
|
||||
group: parent,
|
||||
scale: factor,
|
||||
})
|
||||
}
|
||||
}
|
||||
const isSelected = parent?.userData?.isSelected
|
||||
@ -1825,6 +2211,7 @@ function colorSegment(object: any, color: number) {
|
||||
const straightSegmentBody = getParentGroup(object, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
CIRCLE_SEGMENT,
|
||||
])
|
||||
if (straightSegmentBody) {
|
||||
straightSegmentBody.traverse((child) => {
|
||||
|
@ -24,6 +24,10 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js
|
||||
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
||||
import {
|
||||
CIRCLE_CENTER_HANDLE,
|
||||
CIRCLE_SEGMENT,
|
||||
CIRCLE_SEGMENT_BODY,
|
||||
CIRCLE_SEGMENT_DASH,
|
||||
EXTRA_SEGMENT_HANDLE,
|
||||
EXTRA_SEGMENT_OFFSET_PX,
|
||||
HIDE_SEGMENT_LENGTH,
|
||||
@ -46,7 +50,7 @@ import {
|
||||
import { Themes, getThemeColorForThreeJs } from 'lib/theme'
|
||||
import { roundOff } from 'lib/utils'
|
||||
|
||||
export function profileStart({
|
||||
export function createProfileStartHandle({
|
||||
from,
|
||||
id,
|
||||
pathToNode,
|
||||
@ -225,6 +229,28 @@ function createArrowhead(scale = 1, theme: Themes, color?: number): Group {
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
return arrowGroup
|
||||
}
|
||||
function createCircleCenterHandle(
|
||||
scale = 1,
|
||||
theme: Themes,
|
||||
color?: number
|
||||
): Group {
|
||||
const circleCenterGroup = new Group()
|
||||
|
||||
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const body = new MeshBasicMaterial({ color })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
|
||||
circleCenterGroup.add(mesh)
|
||||
|
||||
circleCenterGroup.userData = {
|
||||
type: CIRCLE_CENTER_HANDLE,
|
||||
baseColor,
|
||||
}
|
||||
circleCenterGroup.name = CIRCLE_CENTER_HANDLE
|
||||
circleCenterGroup.scale.set(scale, scale, scale)
|
||||
return circleCenterGroup
|
||||
}
|
||||
|
||||
function createExtraSegmentHandle(
|
||||
scale: number,
|
||||
@ -300,6 +326,103 @@ function createLengthIndicator({
|
||||
return lengthIndicatorGroup
|
||||
}
|
||||
|
||||
export function circleSegment({
|
||||
prevSegment,
|
||||
from,
|
||||
to,
|
||||
center,
|
||||
radius,
|
||||
id,
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
texture,
|
||||
theme,
|
||||
isSelected,
|
||||
}: {
|
||||
prevSegment: SketchGroup['value'][number]
|
||||
from: Coords2d
|
||||
center: Coords2d
|
||||
radius: number
|
||||
to: Coords2d
|
||||
id: string
|
||||
pathToNode: PathToNode
|
||||
isDraftSegment?: boolean
|
||||
scale?: number
|
||||
texture: Texture
|
||||
theme: Themes
|
||||
isSelected?: boolean
|
||||
}): Group {
|
||||
const group = new Group()
|
||||
|
||||
const geometry = createArcGeometry({
|
||||
center,
|
||||
radius,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
isDashed: isDraftSegment,
|
||||
scale,
|
||||
})
|
||||
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
const body = new MeshBasicMaterial({ color })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
mesh.userData.type = isDraftSegment
|
||||
? CIRCLE_SEGMENT_DASH
|
||||
: CIRCLE_SEGMENT_BODY
|
||||
|
||||
group.userData = {
|
||||
type: CIRCLE_SEGMENT,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
radius,
|
||||
center,
|
||||
ccw: true,
|
||||
prevSegment,
|
||||
pathToNode,
|
||||
isSelected,
|
||||
baseColor,
|
||||
}
|
||||
group.name = CIRCLE_SEGMENT
|
||||
|
||||
const arrowGroup = createArrowhead(scale, theme, color)
|
||||
arrowGroup.position.set(
|
||||
center[0] + Math.cos(Math.PI / 4) * radius,
|
||||
center[1] + Math.sin(Math.PI / 4) * radius,
|
||||
0
|
||||
)
|
||||
|
||||
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
|
||||
circleCenterGroup.position.set(center[0], center[1], 0)
|
||||
const arrowheadAngle = Math.PI / 4
|
||||
arrowGroup.quaternion.setFromUnitVectors(
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
||||
)
|
||||
const pxLength = (radius * 2 * Math.PI) / scale
|
||||
const shouldHide = pxLength < HIDE_SEGMENT_LENGTH
|
||||
|
||||
const extraSegmentGroup = createExtraSegmentHandle(scale, texture, theme)
|
||||
const extraSegmentAngle = 0
|
||||
const extraSegmentOffset = new Vector2(
|
||||
Math.cos(extraSegmentAngle) * radius,
|
||||
Math.sin(extraSegmentAngle) * radius
|
||||
)
|
||||
extraSegmentGroup.position.set(
|
||||
center[0] + extraSegmentOffset.x,
|
||||
center[1] + extraSegmentOffset.y,
|
||||
0
|
||||
)
|
||||
|
||||
extraSegmentGroup.visible = !shouldHide
|
||||
|
||||
group.add(mesh, arrowGroup, circleCenterGroup, extraSegmentGroup)
|
||||
|
||||
return group
|
||||
}
|
||||
export function tangentialArcToSegment({
|
||||
prevSegment,
|
||||
from,
|
||||
|
@ -1611,6 +1611,37 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
|
||||
tangentialArcTo,
|
||||
} as const
|
||||
|
||||
export function changeCircleArguments(
|
||||
node: Program,
|
||||
programMemory: ProgramMemory,
|
||||
sourceRange: SourceRange,
|
||||
center: [number, number],
|
||||
radius: number
|
||||
): { modifiedAst: Program; pathToNode: PathToNode } | Error {
|
||||
const _node = { ...node }
|
||||
const thePath = getNodePathFromSourceRange(_node, sourceRange)
|
||||
const nodeMeta = getNodeFromPath<CallExpression>(_node, thePath)
|
||||
if (err(nodeMeta)) return nodeMeta
|
||||
|
||||
const { node: callExpression, shallowPath } = nodeMeta
|
||||
|
||||
if (callExpression?.callee?.name === 'circle') {
|
||||
const newCenter = createArrayExpression([
|
||||
createLiteral(roundOff(center[0])),
|
||||
createLiteral(roundOff(center[1])),
|
||||
])
|
||||
const newRadius = createLiteral(roundOff(radius))
|
||||
callExpression.arguments[0] = newCenter
|
||||
callExpression.arguments[1] = newRadius
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: shallowPath,
|
||||
}
|
||||
}
|
||||
|
||||
return new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
|
||||
}
|
||||
|
||||
export function changeSketchArguments(
|
||||
node: Program,
|
||||
programMemory: ProgramMemory,
|
||||
|
@ -1415,6 +1415,19 @@ pub enum Path {
|
||||
/// arc's direction
|
||||
ccw: bool,
|
||||
},
|
||||
/// a complete arc
|
||||
Circle {
|
||||
#[serde(flatten)]
|
||||
base: BasePath,
|
||||
/// the arc's center
|
||||
#[ts(type = "[number, number]")]
|
||||
center: [f64; 2],
|
||||
/// the arc's radius
|
||||
radius: f64,
|
||||
/// arc's direction
|
||||
// Maybe this one's not needed since it's a full revolution?
|
||||
ccw: bool,
|
||||
},
|
||||
/// A path that is horizontal.
|
||||
Horizontal {
|
||||
#[serde(flatten)]
|
||||
@ -1447,6 +1460,7 @@ impl Path {
|
||||
Path::Base { base } => base.geo_meta.id,
|
||||
Path::TangentialArcTo { base, .. } => base.geo_meta.id,
|
||||
Path::TangentialArc { base, .. } => base.geo_meta.id,
|
||||
Path::Circle { base, .. } => base.geo_meta.id,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1458,6 +1472,7 @@ impl Path {
|
||||
Path::Base { base } => base.tag.clone(),
|
||||
Path::TangentialArcTo { base, .. } => base.tag.clone(),
|
||||
Path::TangentialArc { base, .. } => base.tag.clone(),
|
||||
Path::Circle { base, .. } => base.tag.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1469,6 +1484,7 @@ impl Path {
|
||||
Path::Base { base } => base,
|
||||
Path::TangentialArcTo { base, .. } => base,
|
||||
Path::TangentialArc { base, .. } => base,
|
||||
Path::Circle { base, .. } => base,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1480,6 +1496,7 @@ impl Path {
|
||||
Path::Base { base } => Some(base),
|
||||
Path::TangentialArcTo { base, .. } => Some(base),
|
||||
Path::TangentialArc { base, .. } => Some(base),
|
||||
Path::Circle { base, .. } => Some(base),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,7 +204,7 @@ pub(crate) async fn do_post_extrude(
|
||||
for path in sketch_group.value.iter() {
|
||||
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
|
||||
match path {
|
||||
Path::TangentialArc { .. } | Path::TangentialArcTo { .. } => {
|
||||
Path::TangentialArc { .. } | Path::TangentialArcTo { .. } | Path::Circle { .. } => {
|
||||
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc {
|
||||
face_id: *actual_face_id,
|
||||
tag: path.get_base().tag.clone(),
|
||||
|
@ -2,16 +2,19 @@
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kittycad::types::{Angle, ModelingCmd};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
ast::types::TagDeclarator,
|
||||
errors::KclError,
|
||||
executor::KclValue,
|
||||
std::{Args, SketchGroup, SketchSurface},
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{BasePath, GeoMeta, KclValue, Path, Point2d, SketchGroup, SketchSurface},
|
||||
std::Args,
|
||||
};
|
||||
|
||||
use super::utils::arc_center_and_end;
|
||||
|
||||
/// A sketch surface or a sketch group.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
@ -65,23 +68,70 @@ async fn inner_circle(
|
||||
SketchSurfaceOrGroup::SketchSurface(surface) => surface,
|
||||
SketchSurfaceOrGroup::SketchGroup(group) => group.on,
|
||||
};
|
||||
let mut sketch_group =
|
||||
let sketch_group =
|
||||
crate::std::sketch::inner_start_profile_at([center[0] + radius, center[1]], sketch_surface, None, args.clone())
|
||||
.await?;
|
||||
|
||||
// Call arc.
|
||||
sketch_group = crate::std::sketch::inner_arc(
|
||||
crate::std::sketch::ArcData::AnglesAndRadius {
|
||||
angle_start: 0.0,
|
||||
angle_end: 360.0,
|
||||
radius,
|
||||
let from: Point2d = sketch_group.current_pen_position()?;
|
||||
|
||||
let angle_start = Angle::from_degrees(0.0);
|
||||
let angle_end = Angle::from_degrees(360.0);
|
||||
let (center, end) = arc_center_and_end(from, angle_start, angle_end, radius);
|
||||
|
||||
if angle_start == angle_end {
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
message: "Arc start and end angles must be different".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
let id = uuid::Uuid::new_v4();
|
||||
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ExtendPath {
|
||||
path: sketch_group.id,
|
||||
segment: kittycad::types::PathSegment::Arc {
|
||||
start: angle_start,
|
||||
end: angle_end,
|
||||
center: center.into(),
|
||||
radius,
|
||||
relative: false,
|
||||
},
|
||||
},
|
||||
sketch_group,
|
||||
tag,
|
||||
args.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Call close.
|
||||
crate::std::sketch::inner_close(sketch_group, None, args).await
|
||||
let current_path = Path::Circle {
|
||||
base: BasePath {
|
||||
from: center.into(),
|
||||
// to: end.into(),
|
||||
to: center.into(),
|
||||
tag: tag.clone(),
|
||||
geo_meta: GeoMeta {
|
||||
id,
|
||||
metadata: args.source_range.into(),
|
||||
},
|
||||
},
|
||||
radius,
|
||||
center: center.into(),
|
||||
ccw: angle_start.degrees() < angle_end.degrees(),
|
||||
};
|
||||
|
||||
let mut new_sketch_group = sketch_group.clone();
|
||||
if let Some(tag) = &tag {
|
||||
new_sketch_group.add_tag(tag, ¤t_path);
|
||||
}
|
||||
|
||||
new_sketch_group.value.push(current_path);
|
||||
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::ClosePath {
|
||||
path_id: new_sketch_group.id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(new_sketch_group)
|
||||
}
|
||||
|
Reference in New Issue
Block a user