New segments can be added in the middle of a sketch (#1953)
* get branch up to where it was before * setup dots properly * only show extra handle on hover * use partical texture for plus button * fix regression * fix deleted line * fix sketch on face test * caluclate segment length in screen-space/in-pixels * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * side small segment handles on resize * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Make sure this works on setup and update of segments * Add to tangential arcs * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commit5dc1adacae
. * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commitb8ceea179c
. * try and fix sketch on face in CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * more test fix * convert scaling to be based on pixels * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * trigger ci * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commit6287c943dd
. * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commit1baa3819db
. * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Update src/clientSideScene/segments.ts Co-authored-by: Frank Noirot <frank@zoo.dev> * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * reduce line thickness * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * trigger CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * trigger CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * trigger CI * try putting init script back in --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Frank Noirot <frank@zoo.dev>
@ -1496,9 +1496,13 @@ test('Sketch on face', async ({ page, context }) => {
|
||||
await page.getByText('startProfileAt([1.03, 1.03], %)').click()
|
||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.setViewportSize({ width: 1200, height: 1200 })
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.updateCamPosition([452, -152, 1166])
|
||||
await u.closeDebugPanel()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
const pointToDragFirst = [691, 237]
|
||||
const pointToDragFirst = [787, 565]
|
||||
await page.mouse.move(pointToDragFirst[0], pointToDragFirst[1])
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(pointToDragFirst[0] - 20, pointToDragFirst[1], {
|
||||
@ -1512,7 +1516,9 @@ test('Sketch on face', async ({ page, context }) => {
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||
|> startProfileAt([1.03, 1.03], %)
|
||||
|> line([2.81, -0.33], %)
|
||||
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
|
||||
process?.env?.CI ? 0.24 : 0.2
|
||||
}], %)
|
||||
|> line([-4.44, -2.13], %)
|
||||
|> close(%)`)
|
||||
|
||||
@ -1535,7 +1541,9 @@ test('Sketch on face', async ({ page, context }) => {
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toContainText(`const part002 = startSketchOn(part001, 'seg01')
|
||||
|> startProfileAt([1.03, 1.03], %)
|
||||
|> line([2.81, -0.33], %)
|
||||
|> line([${process?.env?.CI ? 2.74 : 2.93}, -${
|
||||
process?.env?.CI ? 0.24 : 0.2
|
||||
}], %)
|
||||
|> line([-4.44, -2.13], %)
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)`)
|
||||
|
@ -320,6 +320,22 @@ test('extrude on each default plane should be stable', async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'SETTINGS_PERSIST_KEY',
|
||||
JSON.stringify({
|
||||
baseUnit: 'in',
|
||||
cameraControls: 'KittyCAD',
|
||||
defaultDirectory: '',
|
||||
defaultProjectName: 'project-$nnn',
|
||||
onboardingStatus: 'dismissed',
|
||||
showDebugPanel: true,
|
||||
textWrapping: 'On',
|
||||
theme: 'dark',
|
||||
unitSystem: 'imperial',
|
||||
})
|
||||
)
|
||||
})
|
||||
const u = getUtils(page)
|
||||
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([7.00, 4.40], %)
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 50 KiB |
BIN
public/clientSideSceneAssets/extra-segment-texture.png
Normal file
After Width: | Height: | Size: 327 B |
@ -28,12 +28,15 @@ export function createGridHelper({
|
||||
gridHelper.rotation.x = Math.PI / 2
|
||||
return gridHelper
|
||||
}
|
||||
const fudgeFactor = 72.66985970437086
|
||||
|
||||
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
||||
0.55 / cam.zoom
|
||||
(0.55 * fudgeFactor) / cam.zoom / window.innerHeight
|
||||
|
||||
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
|
||||
(group.position.distanceTo(cam.position) * cam.fov) / 4000
|
||||
(group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) /
|
||||
4000 /
|
||||
window.innerHeight
|
||||
|
||||
export function isQuaternionVertical(q: Quaternion) {
|
||||
const v = new Vector3(0, 0, 1).applyQuaternion(q)
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
PlaneGeometry,
|
||||
Points,
|
||||
Quaternion,
|
||||
Scene,
|
||||
Shape,
|
||||
@ -87,14 +88,18 @@ import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
|
||||
type DraftSegment = 'line' | 'tangentialArcTo'
|
||||
|
||||
export const EXTRA_SEGMENT_HANDLE = 'extraSegmentHandle'
|
||||
export const EXTRA_SEGMENT_OFFSET_PX = 8
|
||||
export const PROFILE_START = 'profile-start'
|
||||
export const STRAIGHT_SEGMENT = 'straight-segment'
|
||||
export const STRAIGHT_SEGMENT_BODY = 'straight-segment-body'
|
||||
export const STRAIGHT_SEGMENT_DASH = 'straight-segment-body-dashed'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
||||
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
|
||||
'tangential-arc-to-segment-body-dashed'
|
||||
export const PROFILE_START = 'profile-start'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT = 'tangential-arc-to-segment'
|
||||
export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
|
||||
export const MIN_SEGMENT_LENGTH = 60 // in pixels
|
||||
export const SEGMENT_WIDTH_PX = 1.6
|
||||
|
||||
// This singleton Class is responsible for all of the things the user sees and interacts with.
|
||||
// That mostly mean sketch elements.
|
||||
@ -111,8 +116,12 @@ export class SceneEntities {
|
||||
this.engineCommandManager = engineCommandManager
|
||||
this.scene = sceneInfra?.scene
|
||||
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
}
|
||||
|
||||
onWindowResize = () => {
|
||||
this.onCamChange()
|
||||
}
|
||||
onCamChange = () => {
|
||||
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
|
||||
|
||||
@ -282,7 +291,6 @@ export class SceneEntities {
|
||||
sketchGroup: SketchGroup
|
||||
variableDeclarationName: string
|
||||
}> {
|
||||
sceneInfra.resetMouseListeners()
|
||||
this.createIntersectionPlane()
|
||||
|
||||
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
|
||||
@ -295,7 +303,7 @@ export class SceneEntities {
|
||||
})
|
||||
const sketchGroup = sketchGroupFromPathToNode({
|
||||
pathToNode: sketchPathToNode,
|
||||
ast: kclManager.ast,
|
||||
ast: maybeModdedAst,
|
||||
programMemory,
|
||||
})
|
||||
if (!Array.isArray(sketchGroup?.value))
|
||||
@ -383,6 +391,7 @@ export class SceneEntities {
|
||||
pathToNode: segPathToNode,
|
||||
isDraftSegment,
|
||||
scale: factor,
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
})
|
||||
} else {
|
||||
seg = straightSegment({
|
||||
@ -393,6 +402,7 @@ export class SceneEntities {
|
||||
isDraftSegment,
|
||||
scale: factor,
|
||||
callExpName,
|
||||
texture: sceneInfra.extraSegmentTexture,
|
||||
})
|
||||
}
|
||||
seg.layers.set(SKETCH_LAYER)
|
||||
@ -435,6 +445,7 @@ export class SceneEntities {
|
||||
) => {
|
||||
await kclManager.updateAst(modifiedAst, false)
|
||||
await this.tearDownSketch({ removeAxis: false })
|
||||
sceneInfra.resetMouseListeners()
|
||||
await this.setupSketch({
|
||||
sketchPathToNode,
|
||||
forward,
|
||||
@ -442,7 +453,12 @@ export class SceneEntities {
|
||||
position: origin,
|
||||
maybeModdedAst: kclManager.ast,
|
||||
})
|
||||
this.setupSketchIdleCallbacks(sketchPathToNode)
|
||||
this.setupSketchIdleCallbacks({
|
||||
forward,
|
||||
up,
|
||||
position: origin,
|
||||
pathToNode: sketchPathToNode,
|
||||
})
|
||||
}
|
||||
setUpDraftSegment = async (
|
||||
sketchPathToNode: PathToNode,
|
||||
@ -467,19 +483,20 @@ export class SceneEntities {
|
||||
|
||||
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
|
||||
|
||||
let modifiedAst = addNewSketchLn({
|
||||
node: kclManager.ast,
|
||||
const mod = addNewSketchLn({
|
||||
node: _ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
to: [lastSeg.to[0], lastSeg.to[1]],
|
||||
from: [lastSeg.to[0], lastSeg.to[1]],
|
||||
fnName: segmentName,
|
||||
pathToNode: sketchPathToNode,
|
||||
}).modifiedAst
|
||||
modifiedAst = parse(recast(modifiedAst))
|
||||
})
|
||||
const modifiedAst = parse(recast(mod.modifiedAst))
|
||||
|
||||
const draftExpressionsIndices = { start: index, end: index }
|
||||
|
||||
if (shouldTearDown) await this.tearDownSketch({ removeAxis: false })
|
||||
sceneInfra.resetMouseListeners()
|
||||
const { truncatedAst, programMemoryOverride, sketchGroup } =
|
||||
await this.setupSketch({
|
||||
sketchPathToNode,
|
||||
@ -549,10 +566,101 @@ export class SceneEntities {
|
||||
...mouseEnterLeaveCallbacks(),
|
||||
})
|
||||
}
|
||||
setupSketchIdleCallbacks = (pathToNode: PathToNode) => {
|
||||
setupSketchIdleCallbacks = ({
|
||||
pathToNode,
|
||||
up,
|
||||
forward,
|
||||
position,
|
||||
}: {
|
||||
pathToNode: PathToNode
|
||||
forward: [number, number, number]
|
||||
up: [number, number, number]
|
||||
position?: [number, number, number]
|
||||
}) => {
|
||||
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
||||
sceneInfra.setCallbacks({
|
||||
onDrag: ({ selected, intersectionPoint, mouseEvent, intersects }) => {
|
||||
onDragEnd: async () => {
|
||||
if (addingNewSegmentStatus !== 'nothing') {
|
||||
await this.tearDownSketch({ removeAxis: false })
|
||||
this.setupSketch({
|
||||
sketchPathToNode: pathToNode,
|
||||
maybeModdedAst: kclManager.ast,
|
||||
up,
|
||||
forward,
|
||||
position,
|
||||
})
|
||||
// setting up the callbacks again resets value in closures
|
||||
this.setupSketchIdleCallbacks({
|
||||
pathToNode,
|
||||
up,
|
||||
forward,
|
||||
position,
|
||||
})
|
||||
}
|
||||
},
|
||||
onDrag: async ({
|
||||
selected,
|
||||
intersectionPoint,
|
||||
mouseEvent,
|
||||
intersects,
|
||||
}) => {
|
||||
if (mouseEvent.which !== 1) return
|
||||
|
||||
const group = getParentGroup(selected, [EXTRA_SEGMENT_HANDLE])
|
||||
if (group?.name === EXTRA_SEGMENT_HANDLE) {
|
||||
const segGroup = getParentGroup(selected)
|
||||
const pathToNode: PathToNode = segGroup?.userData?.pathToNode
|
||||
const pathToNodeIndex = pathToNode.findIndex(
|
||||
(x) => x[1] === 'PipeExpression'
|
||||
)
|
||||
|
||||
const sketchGroup = sketchGroupFromPathToNode({
|
||||
pathToNode,
|
||||
ast: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
})
|
||||
|
||||
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
|
||||
if (addingNewSegmentStatus === 'nothing') {
|
||||
const prevSegment = sketchGroup.value[pipeIndex - 2]
|
||||
const mod = addNewSketchLn({
|
||||
node: kclManager.ast,
|
||||
programMemory: kclManager.programMemory,
|
||||
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
|
||||
from: [prevSegment.from[0], prevSegment.from[1]],
|
||||
// TODO assuming it's always a straight segments being added
|
||||
// as this is easiest, and we'll need to add "tabbing" behavior
|
||||
// to support other segment types
|
||||
fnName: 'line',
|
||||
pathToNode: pathToNode,
|
||||
spliceBetween: true,
|
||||
})
|
||||
addingNewSegmentStatus = 'pending'
|
||||
await kclManager.executeAstMock(mod.modifiedAst, {
|
||||
updates: 'code',
|
||||
})
|
||||
await this.tearDownSketch({ removeAxis: false })
|
||||
this.setupSketch({
|
||||
sketchPathToNode: pathToNode,
|
||||
maybeModdedAst: kclManager.ast,
|
||||
up,
|
||||
forward,
|
||||
position,
|
||||
})
|
||||
addingNewSegmentStatus = 'added'
|
||||
} else if (addingNewSegmentStatus === 'added') {
|
||||
const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex)
|
||||
pathToNodeForNewSegment.push([pipeIndex - 2, 'index'])
|
||||
this.onDragSegment({
|
||||
sketchPathToNode: pathToNodeForNewSegment,
|
||||
object: selected,
|
||||
intersection2d: intersectionPoint.twoD,
|
||||
intersects,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.onDragSegment({
|
||||
object: selected,
|
||||
intersection2d: intersectionPoint.twoD,
|
||||
@ -755,8 +863,7 @@ export class SceneEntities {
|
||||
group.userData.to = to
|
||||
group.userData.prevSegment = prevSegment
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
|
||||
const previousPoint =
|
||||
prevSegment?.type === 'TangentialArcTo'
|
||||
@ -774,13 +881,40 @@ export class SceneEntities {
|
||||
obtuse: true,
|
||||
})
|
||||
|
||||
const arrowheadAngle =
|
||||
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
|
||||
arrowGroup.quaternion.setFromUnitVectors(
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
||||
)
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
const pxLength = arcInfo.arcLength / scale
|
||||
const shouldHide = pxLength < MIN_SEGMENT_LENGTH
|
||||
|
||||
if (arrowGroup) {
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
|
||||
const arrowheadAngle =
|
||||
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
|
||||
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 = !shouldHide
|
||||
}
|
||||
|
||||
if (extraSegmentGroup) {
|
||||
const circumferenceInPx = (2 * Math.PI * arcInfo.radius) / scale
|
||||
const extraSegmentAngleDelta =
|
||||
(EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2
|
||||
const extraSegmentAngle =
|
||||
arcInfo.startAngle + (arcInfo.ccw ? 1 : -1) * extraSegmentAngleDelta
|
||||
const extraSegmentOffset = new Vector2(
|
||||
Math.cos(extraSegmentAngle) * arcInfo.radius,
|
||||
Math.sin(extraSegmentAngle) * arcInfo.radius
|
||||
)
|
||||
extraSegmentGroup.position.set(
|
||||
arcInfo.center[0] + extraSegmentOffset.x,
|
||||
arcInfo.center[1] + extraSegmentOffset.y,
|
||||
0
|
||||
)
|
||||
extraSegmentGroup.scale.set(scale, scale, scale)
|
||||
extraSegmentGroup.visible = !shouldHide
|
||||
}
|
||||
|
||||
const tangentialArcToSegmentBody = group.children.find(
|
||||
(child) => child.userData.type === TANGENTIAL_ARC_TO_SEGMENT_BODY
|
||||
@ -827,10 +961,17 @@ export class SceneEntities {
|
||||
group.userData.from = from
|
||||
group.userData.to = to
|
||||
const shape = new Shape()
|
||||
shape.moveTo(0, -0.08 * scale)
|
||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
|
||||
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
|
||||
const length = Math.sqrt(
|
||||
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
|
||||
)
|
||||
|
||||
const pxLength = length / scale
|
||||
const shouldHide = pxLength < MIN_SEGMENT_LENGTH
|
||||
|
||||
if (arrowGroup) {
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
|
||||
@ -842,6 +983,21 @@ export class SceneEntities {
|
||||
.normalize()
|
||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||
arrowGroup.scale.set(scale, scale, scale)
|
||||
arrowGroup.visible = !shouldHide
|
||||
}
|
||||
|
||||
const extraSegmentGroup = group.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
if (extraSegmentGroup) {
|
||||
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
|
||||
.normalize()
|
||||
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale)
|
||||
extraSegmentGroup.position.set(
|
||||
from[0] + offsetFromBase.x,
|
||||
from[1] + offsetFromBase.y,
|
||||
0
|
||||
)
|
||||
extraSegmentGroup.scale.set(scale, scale, scale)
|
||||
extraSegmentGroup.visible = !shouldHide
|
||||
}
|
||||
|
||||
const straightSegmentBody = group.children.find(
|
||||
@ -1160,7 +1316,7 @@ function colorSegment(object: any, color: number) {
|
||||
])
|
||||
if (straightSegmentBody) {
|
||||
straightSegmentBody.traverse((child) => {
|
||||
if (child instanceof Mesh) {
|
||||
if (child instanceof Mesh && !child.userData.ignoreColorChange) {
|
||||
child.material.color.set(color)
|
||||
}
|
||||
})
|
||||
@ -1264,7 +1420,7 @@ function massageFormats(a: any): Vector3 {
|
||||
|
||||
function mouseEnterLeaveCallbacks() {
|
||||
return {
|
||||
onMouseEnter: ({ selected }: OnMouseEnterLeaveArgs) => {
|
||||
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
|
||||
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
|
||||
const obj = selected as Mesh
|
||||
const mat = obj.material as MeshBasicMaterial
|
||||
@ -1286,6 +1442,14 @@ function mouseEnterLeaveCallbacks() {
|
||||
sceneInfra.highlightCallback([node.start, node.end])
|
||||
const yellow = 0xffff00
|
||||
colorSegment(selected, yellow)
|
||||
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
if (extraSegmentGroup) {
|
||||
extraSegmentGroup.traverse((child) => {
|
||||
if (child instanceof Points || child instanceof Mesh) {
|
||||
child.material.opacity = dragSelected ? 0 : 1
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
sceneInfra.highlightCallback([0, 0])
|
||||
@ -1302,6 +1466,14 @@ function mouseEnterLeaveCallbacks() {
|
||||
selected,
|
||||
isSelected ? 0x0000ff : parent?.userData?.baseColor || 0xffffff
|
||||
)
|
||||
const extraSegmentGroup = parent?.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
if (extraSegmentGroup) {
|
||||
extraSegmentGroup.traverse((child) => {
|
||||
if (child instanceof Points || child instanceof Mesh) {
|
||||
child.material.opacity = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
|
||||
const obj = selected as Mesh
|
||||
const mat = obj.material as MeshBasicMaterial
|
||||
|
@ -18,6 +18,8 @@ import {
|
||||
Intersection,
|
||||
Object3D,
|
||||
Object3DEventMap,
|
||||
TextureLoader,
|
||||
Texture,
|
||||
} from 'three'
|
||||
import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
@ -54,6 +56,7 @@ export const ARROWHEAD = 'arrowhead'
|
||||
|
||||
export interface OnMouseEnterLeaveArgs {
|
||||
selected: Object3D<Object3DEventMap>
|
||||
dragSelected?: Object3D<Object3DEventMap>
|
||||
mouseEvent: MouseEvent
|
||||
}
|
||||
|
||||
@ -98,18 +101,25 @@ export class SceneInfra {
|
||||
isFovAnimationInProgress = false
|
||||
_baseUnit: BaseUnit = 'mm'
|
||||
_baseUnitMultiplier = 1
|
||||
extraSegmentTexture: Texture
|
||||
onDragStartCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onDragEndCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
|
||||
onMoveCallback: (arg: OnMoveCallbackArgs) => void = () => {}
|
||||
onClickCallback: (arg: OnClickCallbackArgs) => void = () => {}
|
||||
onMouseEnter: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||
onMouseLeave: (arg: OnMouseEnterLeaveArgs) => void = () => {}
|
||||
setCallbacks = (callbacks: {
|
||||
onDragStart?: (arg: OnDragCallbackArgs) => void
|
||||
onDragEnd?: (arg: OnDragCallbackArgs) => void
|
||||
onDrag?: (arg: OnDragCallbackArgs) => void
|
||||
onMove?: (arg: OnMoveCallbackArgs) => void
|
||||
onClick?: (arg: OnClickCallbackArgs) => void
|
||||
onMouseEnter?: (arg: OnMouseEnterLeaveArgs) => void
|
||||
onMouseLeave?: (arg: OnMouseEnterLeaveArgs) => void
|
||||
}) => {
|
||||
this.onDragStartCallback = callbacks.onDragStart || this.onDragStartCallback
|
||||
this.onDragEndCallback = callbacks.onDragEnd || this.onDragEndCallback
|
||||
this.onDragCallback = callbacks.onDrag || this.onDragCallback
|
||||
this.onMoveCallback = callbacks.onMove || this.onMoveCallback
|
||||
this.onClickCallback = callbacks.onClick || this.onClickCallback
|
||||
@ -128,6 +138,8 @@ export class SceneInfra {
|
||||
}
|
||||
resetMouseListeners = () => {
|
||||
this.setCallbacks({
|
||||
onDragStart: () => {},
|
||||
onDragEnd: () => {},
|
||||
onDrag: () => {},
|
||||
onMove: () => {},
|
||||
onClick: () => {},
|
||||
@ -210,6 +222,13 @@ export class SceneInfra {
|
||||
const light = new AmbientLight(0x505050) // soft white light
|
||||
this.scene.add(light)
|
||||
|
||||
const textureLoader = new TextureLoader()
|
||||
this.extraSegmentTexture = textureLoader.load(
|
||||
'/clientSideSceneAssets/extra-segment-texture.png'
|
||||
)
|
||||
this.extraSegmentTexture.anisotropy =
|
||||
this.renderer?.capabilities?.getMaxAnisotropy?.()
|
||||
|
||||
SceneInfra.instance = this
|
||||
}
|
||||
|
||||
@ -358,6 +377,7 @@ export class SceneInfra {
|
||||
this.hoveredObject = firstIntersectObject
|
||||
this.onMouseEnter({
|
||||
selected: this.hoveredObject,
|
||||
dragSelected: this.selected?.object,
|
||||
mouseEvent: mouseEvent,
|
||||
})
|
||||
}
|
||||
@ -365,6 +385,7 @@ export class SceneInfra {
|
||||
if (this.hoveredObject) {
|
||||
this.onMouseLeave({
|
||||
selected: this.hoveredObject,
|
||||
dragSelected: this.selected?.object,
|
||||
mouseEvent: mouseEvent,
|
||||
})
|
||||
this.hoveredObject = null
|
||||
@ -453,8 +474,16 @@ export class SceneInfra {
|
||||
|
||||
if (this.selected) {
|
||||
if (this.selected.hasBeenDragged) {
|
||||
// this is where we could fire a onDragEnd event
|
||||
// console.log('onDragEnd', this.selected)
|
||||
// TODO do the types properly here
|
||||
this.onDragEndCallback({
|
||||
intersectionPoint: {
|
||||
twoD: planeIntersectPoint?.twoD as any,
|
||||
threeD: planeIntersectPoint?.threeD as any,
|
||||
},
|
||||
intersects,
|
||||
mouseEvent,
|
||||
selected: this.selected as any,
|
||||
})
|
||||
} else if (planeIntersectPoint?.twoD && planeIntersectPoint?.threeD) {
|
||||
// fire onClick event as there was no drags
|
||||
this.onClickCallback({
|
||||
|
@ -12,15 +12,22 @@ import {
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
NormalBufferAttributes,
|
||||
Points,
|
||||
PointsMaterial,
|
||||
Shape,
|
||||
SphereGeometry,
|
||||
Texture,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from 'three'
|
||||
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||
import { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
|
||||
import {
|
||||
EXTRA_SEGMENT_HANDLE,
|
||||
EXTRA_SEGMENT_OFFSET_PX,
|
||||
MIN_SEGMENT_LENGTH,
|
||||
PROFILE_START,
|
||||
SEGMENT_WIDTH_PX,
|
||||
STRAIGHT_SEGMENT,
|
||||
STRAIGHT_SEGMENT_BODY,
|
||||
STRAIGHT_SEGMENT_DASH,
|
||||
@ -44,7 +51,7 @@ export function profileStart({
|
||||
}) {
|
||||
const group = new Group()
|
||||
|
||||
const geometry = new BoxGeometry(0.8, 0.8, 0.8)
|
||||
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
|
||||
const body = new MeshBasicMaterial({ color: 0xffffff })
|
||||
const mesh = new Mesh(geometry, body)
|
||||
|
||||
@ -71,6 +78,7 @@ export function straightSegment({
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
callExpName,
|
||||
texture,
|
||||
}: {
|
||||
from: Coords2d
|
||||
to: Coords2d
|
||||
@ -79,12 +87,13 @@ export function straightSegment({
|
||||
isDraftSegment?: boolean
|
||||
scale?: number
|
||||
callExpName: string
|
||||
texture: Texture
|
||||
}): Group {
|
||||
const group = new Group()
|
||||
|
||||
const shape = new Shape()
|
||||
shape.moveTo(0, -0.08 * scale)
|
||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
|
||||
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
|
||||
|
||||
let geometry
|
||||
if (isDraftSegment) {
|
||||
@ -122,24 +131,44 @@ export function straightSegment({
|
||||
}
|
||||
group.name = STRAIGHT_SEGMENT
|
||||
|
||||
const length = Math.sqrt(
|
||||
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
|
||||
)
|
||||
const arrowGroup = createArrowhead(scale)
|
||||
arrowGroup.position.set(to[0], to[1], 0)
|
||||
const dir = new Vector3()
|
||||
.subVectors(new Vector3(to[0], to[1], 0), new Vector3(from[0], from[1], 0))
|
||||
.normalize()
|
||||
arrowGroup.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir)
|
||||
const pxLength = length / scale
|
||||
const shouldHide = pxLength < MIN_SEGMENT_LENGTH
|
||||
arrowGroup.visible = !shouldHide
|
||||
|
||||
group.add(mesh)
|
||||
if (callExpName !== 'close') group.add(arrowGroup)
|
||||
|
||||
const extraSegmentGroup = createExtraSegmentHandle(scale, texture)
|
||||
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1])
|
||||
.normalize()
|
||||
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale)
|
||||
extraSegmentGroup.position.set(
|
||||
from[0] + offsetFromBase.x,
|
||||
from[1] + offsetFromBase.y,
|
||||
0
|
||||
)
|
||||
extraSegmentGroup.visible = !shouldHide
|
||||
group.add(extraSegmentGroup)
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
function createArrowhead(scale = 1): Group {
|
||||
const arrowMaterial = new MeshBasicMaterial({ color: 0xffffff })
|
||||
const arrowheadMesh = new Mesh(new ConeGeometry(0.31, 1.5, 12), arrowMaterial)
|
||||
arrowheadMesh.position.set(0, -0.6, 0)
|
||||
const sphereMesh = new Mesh(new SphereGeometry(0.27, 12, 12), arrowMaterial)
|
||||
// specify the size of the geometry in pixels (i.e. cone height = 20px, cone radius = 4.5px)
|
||||
// we'll scale the group to the correct size later to match these sizes in screen space
|
||||
const arrowheadMesh = new Mesh(new ConeGeometry(4.5, 20, 12), arrowMaterial)
|
||||
arrowheadMesh.position.set(0, -9, 0)
|
||||
const sphereMesh = new Mesh(new SphereGeometry(4, 12, 12), arrowMaterial)
|
||||
|
||||
const arrowGroup = new Group()
|
||||
arrowGroup.userData.type = ARROWHEAD
|
||||
@ -150,6 +179,36 @@ function createArrowhead(scale = 1): Group {
|
||||
return arrowGroup
|
||||
}
|
||||
|
||||
function createExtraSegmentHandle(scale: number, texture: Texture): Group {
|
||||
const particleMaterial = new PointsMaterial({
|
||||
size: 12, // in pixels
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
depthTest: false,
|
||||
})
|
||||
const mat = new MeshBasicMaterial({
|
||||
transparent: true,
|
||||
color: 0xffffff,
|
||||
opacity: 0,
|
||||
})
|
||||
const particleGeometry = new BufferGeometry().setFromPoints([
|
||||
new Vector3(0, 0, 0),
|
||||
])
|
||||
const sphereMesh = new Mesh(new SphereGeometry(6, 12, 12), mat) // sphere radius in pixels
|
||||
const particle = new Points(particleGeometry, particleMaterial)
|
||||
particle.userData.ignoreColorChange = true
|
||||
particle.userData.type = EXTRA_SEGMENT_HANDLE
|
||||
|
||||
const extraSegmentGroup = new Group()
|
||||
extraSegmentGroup.userData.type = EXTRA_SEGMENT_HANDLE
|
||||
extraSegmentGroup.name = EXTRA_SEGMENT_HANDLE
|
||||
extraSegmentGroup.add(sphereMesh)
|
||||
extraSegmentGroup.add(particle)
|
||||
extraSegmentGroup.scale.set(scale, scale, scale)
|
||||
return extraSegmentGroup
|
||||
}
|
||||
|
||||
export function tangentialArcToSegment({
|
||||
prevSegment,
|
||||
from,
|
||||
@ -158,6 +217,7 @@ export function tangentialArcToSegment({
|
||||
pathToNode,
|
||||
isDraftSegment,
|
||||
scale = 1,
|
||||
texture,
|
||||
}: {
|
||||
prevSegment: SketchGroup['value'][number]
|
||||
from: Coords2d
|
||||
@ -166,6 +226,7 @@ export function tangentialArcToSegment({
|
||||
pathToNode: PathToNode
|
||||
isDraftSegment?: boolean
|
||||
scale?: number
|
||||
texture: Texture
|
||||
}): Group {
|
||||
const group = new Group()
|
||||
|
||||
@ -178,12 +239,13 @@ export function tangentialArcToSegment({
|
||||
)
|
||||
: prevSegment.from
|
||||
|
||||
const { center, radius, startAngle, endAngle, ccw } = getTangentialArcToInfo({
|
||||
arcStartPoint: from,
|
||||
arcEndPoint: to,
|
||||
tanPreviousPoint: previousPoint,
|
||||
obtuse: true,
|
||||
})
|
||||
const { center, radius, startAngle, endAngle, ccw, arcLength } =
|
||||
getTangentialArcToInfo({
|
||||
arcStartPoint: from,
|
||||
arcEndPoint: to,
|
||||
tanPreviousPoint: previousPoint,
|
||||
obtuse: true,
|
||||
})
|
||||
|
||||
const geometry = createArcGeometry({
|
||||
center,
|
||||
@ -219,8 +281,28 @@ export function tangentialArcToSegment({
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(Math.cos(arrowheadAngle), Math.sin(arrowheadAngle), 0)
|
||||
)
|
||||
const pxLength = arcLength / scale
|
||||
const shouldHide = pxLength < MIN_SEGMENT_LENGTH
|
||||
arrowGroup.visible = !shouldHide
|
||||
|
||||
group.add(mesh, arrowGroup)
|
||||
const extraSegmentGroup = createExtraSegmentHandle(scale, texture)
|
||||
const circumferenceInPx = (2 * Math.PI * radius) / scale
|
||||
const extraSegmentAngleDelta =
|
||||
(EXTRA_SEGMENT_OFFSET_PX / circumferenceInPx) * Math.PI * 2
|
||||
const extraSegmentAngle = startAngle + (ccw ? 1 : -1) * extraSegmentAngleDelta
|
||||
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, extraSegmentGroup)
|
||||
|
||||
return group
|
||||
}
|
||||
@ -242,8 +324,8 @@ export function createArcGeometry({
|
||||
isDashed?: boolean
|
||||
scale?: number
|
||||
}): BufferGeometry {
|
||||
const dashSize = 1.2 * scale
|
||||
const gapSize = 1.2 * scale
|
||||
const dashSizePx = 18 * scale
|
||||
const gapSizePx = 18 * scale
|
||||
const arcStart = new EllipseCurve(
|
||||
center[0],
|
||||
center[1],
|
||||
@ -265,8 +347,8 @@ export function createArcGeometry({
|
||||
0
|
||||
)
|
||||
const shape = new Shape()
|
||||
shape.moveTo(0, -0.08 * scale)
|
||||
shape.lineTo(0, 0.08 * scale) // The width of the line
|
||||
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
|
||||
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale) // The width of the line
|
||||
|
||||
if (!isDashed) {
|
||||
const points = arcStart.getPoints(50)
|
||||
@ -281,7 +363,7 @@ export function createArcGeometry({
|
||||
}
|
||||
|
||||
const length = arcStart.getLength()
|
||||
const totalDashes = length / (dashSize + gapSize) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
|
||||
const totalDashes = length / (dashSizePx + gapSizePx) // rounding makes the dashes jittery since the new dash is suddenly appears instead of growing into place
|
||||
const dashesAtEachEnd = Math.min(100, totalDashes / 2) // Assuming we want 50 dashes total, 25 at each end
|
||||
|
||||
const dashGeometries = []
|
||||
@ -289,8 +371,8 @@ export function createArcGeometry({
|
||||
// Function to create a dash at a specific t value (0 to 1 along the curve)
|
||||
const createDashAt = (t: number, curve: EllipseCurve) => {
|
||||
const startVec = curve.getPoint(t)
|
||||
const endVec = curve.getPoint(Math.min(0.5, t + dashSize / length))
|
||||
const midVec = curve.getPoint(Math.min(0.5, t + dashSize / length / 2))
|
||||
const endVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length))
|
||||
const midVec = curve.getPoint(Math.min(0.5, t + dashSizePx / length / 2))
|
||||
const dashCurve = new CurvePath<Vector3>()
|
||||
dashCurve.add(
|
||||
new CatmullRomCurve3([
|
||||
@ -314,7 +396,8 @@ export function createArcGeometry({
|
||||
}
|
||||
|
||||
// fill in the remaining arc
|
||||
const remainingArcLength = length - dashesAtEachEnd * 2 * (dashSize + gapSize)
|
||||
const remainingArcLength =
|
||||
length - dashesAtEachEnd * 2 * (dashSizePx + gapSizePx)
|
||||
if (remainingArcLength > 0) {
|
||||
const remainingArcStartT = dashesAtEachEnd / totalDashes
|
||||
const remainingArcEndT = 1 - remainingArcStartT
|
||||
@ -359,8 +442,8 @@ export function dashedStraight(
|
||||
shape: Shape,
|
||||
scale = 1
|
||||
): BufferGeometry<NormalBufferAttributes> {
|
||||
const dashSize = 1.2 * scale
|
||||
const gapSize = 1.2 * scale // todo: gabSize is not respected
|
||||
const dashSize = 18 * scale
|
||||
const gapSize = 18 * scale // TODO: gapSize is not respected
|
||||
const dashLine = new LineCurve3(
|
||||
new Vector3(from[0], from[1], 0),
|
||||
new Vector3(to[0], to[1], 0)
|
||||
|
@ -162,6 +162,7 @@ export const line: SketchLineHelper = {
|
||||
replaceExisting,
|
||||
referencedSegment,
|
||||
createCallback,
|
||||
spliceBetween,
|
||||
}) => {
|
||||
const _node = { ...node }
|
||||
const { node: pipe } = getNodeFromPath<PipeExpression | CallExpression>(
|
||||
@ -178,6 +179,30 @@ export const line: SketchLineHelper = {
|
||||
const newXVal = createLiteral(roundOff(to[0] - from[0], 2))
|
||||
const newYVal = createLiteral(roundOff(to[1] - from[1], 2))
|
||||
|
||||
if (spliceBetween && !createCallback && pipe.type === 'PipeExpression') {
|
||||
const callExp = createCallExpression('line', [
|
||||
createArrayExpression([newXVal, newYVal]),
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
const pathToNodeIndex = pathToNode.findIndex(
|
||||
(x) => x[1] === 'PipeExpression'
|
||||
)
|
||||
const pipeIndex = pathToNode[pathToNodeIndex + 1][0]
|
||||
if (typeof pipeIndex === 'undefined' || typeof pipeIndex === 'string') {
|
||||
throw new Error('pipeIndex is undefined')
|
||||
// return
|
||||
}
|
||||
pipe.body = [
|
||||
...pipe.body.slice(0, pipeIndex),
|
||||
callExp,
|
||||
...pipe.body.slice(pipeIndex),
|
||||
]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
}
|
||||
}
|
||||
|
||||
if (replaceExisting && createCallback && pipe.type !== 'CallExpression') {
|
||||
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
|
||||
const { callExp, valueUsedInTransform } = createCallback(
|
||||
@ -1023,15 +1048,6 @@ export function changeSketchArguments(
|
||||
throw new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
|
||||
}
|
||||
|
||||
interface CreateLineFnCallArgs {
|
||||
node: Program
|
||||
programMemory: ProgramMemory
|
||||
to: [number, number]
|
||||
from: [number, number]
|
||||
fnName: ToolTip
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
export function compareVec2Epsilon(
|
||||
vec1: [number, number],
|
||||
vec2: [number, number],
|
||||
@ -1056,6 +1072,16 @@ export function compareVec2Epsilon2(
|
||||
return distance < compareEpsilon
|
||||
}
|
||||
|
||||
interface CreateLineFnCallArgs {
|
||||
node: Program
|
||||
programMemory: ProgramMemory
|
||||
to: [number, number]
|
||||
from: [number, number]
|
||||
fnName: ToolTip
|
||||
pathToNode: PathToNode
|
||||
spliceBetween?: boolean
|
||||
}
|
||||
|
||||
export function addNewSketchLn({
|
||||
node: _node,
|
||||
programMemory: previousProgramMemory,
|
||||
@ -1063,6 +1089,7 @@ export function addNewSketchLn({
|
||||
fnName,
|
||||
pathToNode,
|
||||
from,
|
||||
spliceBetween = false,
|
||||
}: CreateLineFnCallArgs): {
|
||||
modifiedAst: Program
|
||||
pathToNode: PathToNode
|
||||
@ -1083,6 +1110,7 @@ export function addNewSketchLn({
|
||||
to,
|
||||
from,
|
||||
replaceExisting: false,
|
||||
spliceBetween,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,8 @@ interface addCall extends ModifyAstBase {
|
||||
referencedSegment?: Path
|
||||
replaceExisting?: boolean
|
||||
createCallback?: TransformCallback // TODO: #29 probably should not be optional
|
||||
/// defaults to false, normal behavior is to add a new callExpression to the end of the pipeExpression
|
||||
spliceBetween?: boolean
|
||||
}
|
||||
|
||||
interface updateArgs extends ModifyAstBase {
|
||||
|
@ -252,6 +252,7 @@ export function getTangentialArcToInfo({
|
||||
startAngle: number
|
||||
endAngle: number
|
||||
ccw: boolean
|
||||
arcLength: number
|
||||
} {
|
||||
const result = get_tangential_arc_to_info(
|
||||
arcStartPoint[0],
|
||||
@ -269,6 +270,7 @@ export function getTangentialArcToInfo({
|
||||
startAngle: result.start_angle,
|
||||
endAngle: result.end_angle,
|
||||
ccw: result.ccw > 0,
|
||||
arcLength: result.arc_length,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -793,6 +793,7 @@ export const modelingMachine = createMachine(
|
||||
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
|
||||
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
}
|
||||
sceneInfra.resetMouseListeners()
|
||||
await sceneEntitiesManager.setupSketch({
|
||||
sketchPathToNode: sketchDetails?.sketchPathToNode || [],
|
||||
forward: sketchDetails.zAxis,
|
||||
@ -800,9 +801,13 @@ export const modelingMachine = createMachine(
|
||||
position: sketchDetails.origin,
|
||||
maybeModdedAst: kclManager.ast,
|
||||
})
|
||||
sceneEntitiesManager.setupSketchIdleCallbacks(
|
||||
sketchDetails?.sketchPathToNode || []
|
||||
)
|
||||
sceneInfra.resetMouseListeners()
|
||||
sceneEntitiesManager.setupSketchIdleCallbacks({
|
||||
pathToNode: sketchDetails?.sketchPathToNode || [],
|
||||
forward: sketchDetails.zAxis,
|
||||
up: sketchDetails.yAxis,
|
||||
position: sketchDetails.origin,
|
||||
})
|
||||
})()
|
||||
},
|
||||
'animate after sketch': () => {
|
||||
|
@ -576,6 +576,8 @@ pub struct TangentialArcInfoOutput {
|
||||
pub end_angle: f64,
|
||||
/// If the arc is counter-clockwise.
|
||||
pub ccw: i32,
|
||||
/// The length of the arc.
|
||||
pub arc_length: f64,
|
||||
}
|
||||
|
||||
// tanPreviousPoint and arcStartPoint make up a straight segment leading into the arc (of which the arc should be tangential). The arc should start at arcStartPoint and end at, arcEndPoint
|
||||
@ -626,6 +628,17 @@ pub fn get_tangential_arc_to_info(input: TangentialArcInfoInput) -> TangentialAr
|
||||
let end_angle = (input.arc_end_point[1] - center[1]).atan2(input.arc_end_point[0] - center[0]);
|
||||
let ccw = is_points_ccw(&[input.arc_start_point, arc_mid_point, input.arc_end_point]);
|
||||
|
||||
let arc_mid_angle = (arc_mid_point[1] - center[1]).atan2(arc_mid_point[0] - center[0]);
|
||||
let start_to_mid_arc_length = radius
|
||||
* delta(Angle::from_radians(start_angle), Angle::from_radians(arc_mid_angle))
|
||||
.radians()
|
||||
.abs();
|
||||
let mid_to_end_arc_length = radius
|
||||
* delta(Angle::from_radians(arc_mid_angle), Angle::from_radians(end_angle))
|
||||
.radians()
|
||||
.abs();
|
||||
let arc_length = start_to_mid_arc_length + mid_to_end_arc_length;
|
||||
|
||||
TangentialArcInfoOutput {
|
||||
center,
|
||||
radius,
|
||||
@ -633,6 +646,7 @@ pub fn get_tangential_arc_to_info(input: TangentialArcInfoInput) -> TangentialAr
|
||||
start_angle,
|
||||
end_angle,
|
||||
ccw,
|
||||
arc_length,
|
||||
}
|
||||
}
|
||||
|
||||
@ -758,6 +772,58 @@ mod get_tangential_arc_to_info_tests {
|
||||
assert_relative_eq!(result.end_angle, -PI / 2.0);
|
||||
assert_eq!(result.ccw, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arc_length_obtuse_cw() {
|
||||
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
|
||||
tan_previous_point: [-1.0, -1.0],
|
||||
arc_start_point: [-1.0, 0.0],
|
||||
arc_end_point: [0.0, -1.0],
|
||||
obtuse: true,
|
||||
});
|
||||
let circumference = 2.0 * PI * result.radius;
|
||||
let expected_length = circumference * 3.0 / 4.0; // 3 quarters of a circle circle
|
||||
assert_relative_eq!(result.arc_length, expected_length);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arc_length_acute_cw() {
|
||||
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
|
||||
tan_previous_point: [-1.0, -1.0],
|
||||
arc_start_point: [-1.0, 0.0],
|
||||
arc_end_point: [0.0, 1.0],
|
||||
obtuse: true,
|
||||
});
|
||||
let circumference = 2.0 * PI * result.radius;
|
||||
let expected_length = circumference / 4.0; // 1 quarters of a circle circle
|
||||
assert_relative_eq!(result.arc_length, expected_length);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arc_length_obtuse_ccw() {
|
||||
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
|
||||
tan_previous_point: [1.0, -1.0],
|
||||
arc_start_point: [1.0, 0.0],
|
||||
arc_end_point: [0.0, -1.0],
|
||||
obtuse: true,
|
||||
});
|
||||
let circumference = 2.0 * PI * result.radius;
|
||||
let expected_length = circumference * 3.0 / 4.0; // 1 quarters of a circle circle
|
||||
assert_relative_eq!(result.arc_length, expected_length);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arc_length_acute_ccw() {
|
||||
let result = get_tangential_arc_to_info(TangentialArcInfoInput {
|
||||
tan_previous_point: [1.0, -1.0],
|
||||
arc_start_point: [1.0, 0.0],
|
||||
arc_end_point: [0.0, 1.0],
|
||||
obtuse: true,
|
||||
});
|
||||
let circumference = 2.0 * PI * result.radius;
|
||||
let expected_length = circumference / 4.0; // 1 quarters of a circle circle
|
||||
assert_relative_eq!(result.arc_length, expected_length);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_tangent_point_from_previous_arc(
|
||||
|
@ -333,6 +333,8 @@ pub struct TangentialArcInfoOutputWasm {
|
||||
pub end_angle: f64,
|
||||
/// Flag to determine if the arc is counter clockwise.
|
||||
pub ccw: i32,
|
||||
/// The length of the arc.
|
||||
pub arc_length: f64,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@ -362,6 +364,7 @@ pub fn get_tangential_arc_to_info(
|
||||
start_angle: result.start_angle,
|
||||
end_angle: result.end_angle,
|
||||
ccw: result.ccw,
|
||||
arc_length: result.arc_length,
|
||||
}
|
||||
}
|
||||
|
||||
|