Add segment length indicators to straight segments in sketch mode (#2935)

* Rough impl of line lengths, still duplicating

* Make sure the labels get cleared along with the rest of the sketch

* Show current units in segment length indicators

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Re-run CI after snapshots

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Re-run CI

* Make sure `close` segments get insert segment handles

* Skip engine connection tests on Safari with a todo

* Fmt

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2024-07-08 16:41:00 -04:00
committed by GitHub
parent ebdaf59d1c
commit 74ec749560
8 changed files with 163 additions and 22 deletions

View File

@ -6721,7 +6721,15 @@ ${extraLine ? 'const myVar = segLen(seg01, part001)' : ''}`
}) })
test.describe('Test network and connection issues', () => { test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({ page }) => { test('simulate network down and network little widget', async ({
page,
browserName,
}) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -6791,7 +6799,15 @@ test.describe('Test network and connection issues', () => {
await expect(page.getByText('Network Health (Connected)')).toBeVisible() await expect(page.getByText('Network Health (Connected)')).toBeVisible()
}) })
test('Engine disconnect & reconnect in sketch mode', async ({ page }) => { test('Engine disconnect & reconnect in sketch mode', async ({
page,
browserName,
}) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
test.skip(
browserName === 'webkit',
'Skip on Safari until `window.tearDown` is working there'
)
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -96,6 +96,7 @@ export const ClientSideScene = ({
if (!canvasRef.current) return if (!canvasRef.current) return
const canvas = canvasRef.current const canvas = canvasRef.current
canvas.appendChild(sceneInfra.renderer.domElement) canvas.appendChild(sceneInfra.renderer.domElement)
canvas.appendChild(sceneInfra.labelRenderer.domElement)
sceneInfra.animate() sceneInfra.animate()
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false) canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false) canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)

View File

@ -29,6 +29,9 @@ import {
INTERSECTION_PLANE_LAYER, INTERSECTION_PLANE_LAYER,
OnMouseEnterLeaveArgs, OnMouseEnterLeaveArgs,
RAYCASTABLE_PLANE, RAYCASTABLE_PLANE,
SEGMENT_LENGTH_LABEL,
SEGMENT_LENGTH_LABEL_OFFSET_PX,
SEGMENT_LENGTH_LABEL_TEXT,
SKETCH_GROUP_SEGMENTS, SKETCH_GROUP_SEGMENTS,
SKETCH_LAYER, SKETCH_LAYER,
X_AXIS, X_AXIS,
@ -102,6 +105,7 @@ import {
} from 'lib/rectangleTool' } from 'lib/rectangleTool'
import { getThemeColorForThreeJs } from 'lib/theme' import { getThemeColorForThreeJs } from 'lib/theme'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
type DraftSegment = 'line' | 'tangentialArcTo' type DraftSegment = 'line' | 'tangentialArcTo'
@ -414,7 +418,7 @@ export class SceneEntities {
} }
) )
let seg let seg: Group
const _node1 = getNodeFromPath<CallExpression>( const _node1 = getNodeFromPath<CallExpression>(
maybeModdedAst, maybeModdedAst,
segPathToNode, segPathToNode,
@ -1302,6 +1306,7 @@ export class SceneEntities {
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case) 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) shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
const labelGroup = group.getObjectByName(SEGMENT_LENGTH_LABEL) as Group
const length = Math.sqrt( const length = Math.sqrt(
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2) Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
@ -1347,6 +1352,29 @@ export class SceneEntities {
extraSegmentGroup.visible = isHandlesVisible extraSegmentGroup.visible = isHandlesVisible
} }
if (labelGroup) {
const labelWrapper = labelGroup.getObjectByName(
SEGMENT_LENGTH_LABEL_TEXT
) as CSS2DObject
const labelWrapperElem = labelWrapper.element as HTMLDivElement
const label = labelWrapperElem.children[0] as HTMLParagraphElement
label.innerText = `${roundOff(length)}${sceneInfra._baseUnit}`
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
const offsetFromMidpoint = new Vector2(to[0] - from[0], to[1] - from[1])
.normalize()
.rotateAround(new Vector2(0, 0), Math.PI / 2)
.multiplyScalar(SEGMENT_LENGTH_LABEL_OFFSET_PX * scale)
label.style.setProperty('--x', `${offsetFromMidpoint.x}px`)
label.style.setProperty('--y', `${offsetFromMidpoint.y}px`)
labelWrapper.position.set(
(from[0] + to[0]) / 2 + offsetFromMidpoint.x,
(from[1] + to[1]) / 2 + offsetFromMidpoint.y,
0
)
labelGroup.visible = isHandlesVisible
}
const straightSegmentBody = group.children.find( const straightSegmentBody = group.children.find(
(child) => child.userData.type === STRAIGHT_SEGMENT_BODY (child) => child.userData.type === STRAIGHT_SEGMENT_BODY
) as Mesh ) as Mesh
@ -1397,6 +1425,14 @@ export class SceneEntities {
) )
let shouldResolve = false let shouldResolve = false
if (sketchSegments) { if (sketchSegments) {
// We have to manually remove the CSS2DObjects
// as they don't get removed when the group is removed
sketchSegments.traverse((object) => {
if (object instanceof CSS2DObject) {
object.element.remove()
object.remove()
}
})
this.scene.remove(sketchSegments) this.scene.remove(sketchSegments)
shouldResolve = true shouldResolve = true
} else { } else {

View File

@ -31,6 +31,7 @@ import { EngineCommandManager } from 'lang/std/engineConnection'
import { MouseState, SegmentOverlayPayload } from 'machines/modelingMachine' import { MouseState, SegmentOverlayPayload } from 'machines/modelingMachine'
import { getAngle, throttle } from 'lib/utils' import { getAngle, throttle } from 'lib/utils'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer'
type SendType = ReturnType<typeof useModelingContext>['send'] type SendType = ReturnType<typeof useModelingContext>['send']
@ -54,6 +55,9 @@ export const Y_AXIS = 'yAxis'
export const AXIS_GROUP = 'axisGroup' export const AXIS_GROUP = 'axisGroup'
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments' export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
export const ARROWHEAD = 'arrowhead' export const ARROWHEAD = 'arrowhead'
export const SEGMENT_LENGTH_LABEL = 'segment-length-label'
export const SEGMENT_LENGTH_LABEL_TEXT = 'segment-length-label-text'
export const SEGMENT_LENGTH_LABEL_OFFSET_PX = 30
export interface OnMouseEnterLeaveArgs { export interface OnMouseEnterLeaveArgs {
selected: Object3D<Object3DEventMap> selected: Object3D<Object3DEventMap>
@ -95,6 +99,7 @@ export class SceneInfra {
static instance: SceneInfra static instance: SceneInfra
scene: Scene scene: Scene
renderer: WebGLRenderer renderer: WebGLRenderer
labelRenderer: CSS2DRenderer
camControls: CameraControls camControls: CameraControls
isPerspective = true isPerspective = true
fov = 45 fov = 45
@ -264,6 +269,13 @@ export class SceneInfra {
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
this.renderer.setSize(window.innerWidth, window.innerHeight) this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent) this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
// LABEL RENDERER
this.labelRenderer = new CSS2DRenderer()
this.labelRenderer.setSize(window.innerWidth, window.innerHeight)
this.labelRenderer.domElement.style.position = 'absolute'
this.labelRenderer.domElement.style.top = '0px'
this.labelRenderer.domElement.style.pointerEvents = 'none'
window.addEventListener('resize', this.onWindowResize) window.addEventListener('resize', this.onWindowResize)
this.camControls = new CameraControls( this.camControls = new CameraControls(
@ -328,6 +340,7 @@ export class SceneInfra {
onWindowResize = () => { onWindowResize = () => {
this.renderer.setSize(window.innerWidth, window.innerHeight) this.renderer.setSize(window.innerWidth, window.innerHeight)
this.labelRenderer.setSize(window.innerWidth, window.innerHeight)
} }
animate = () => { animate = () => {
@ -337,6 +350,7 @@ export class SceneInfra {
// console.log('animation frame', this.cameraControls.camera) // console.log('animation frame', this.cameraControls.camera)
this.camControls.update() this.camControls.update()
this.renderer.render(this.scene, this.camControls.camera) this.renderer.render(this.scene, this.camControls.camera)
this.labelRenderer.render(this.scene, this.camControls.camera)
} }
} }

View File

@ -21,6 +21,7 @@ import {
Vector3, Vector3,
} from 'three' } from 'three'
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' 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 { PathToNode, SketchGroup, getTangentialArcToInfo } from 'lang/wasm'
import { import {
EXTRA_SEGMENT_HANDLE, EXTRA_SEGMENT_HANDLE,
@ -36,8 +37,14 @@ import {
TANGENTIAL_ARC_TO__SEGMENT_DASH, TANGENTIAL_ARC_TO__SEGMENT_DASH,
} from './sceneEntities' } from './sceneEntities'
import { getTangentPointFromPreviousArc } from 'lib/utils2d' import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { ARROWHEAD } from './sceneInfra' import {
ARROWHEAD,
SEGMENT_LENGTH_LABEL,
SEGMENT_LENGTH_LABEL_OFFSET_PX,
SEGMENT_LENGTH_LABEL_TEXT,
} from './sceneInfra'
import { Themes, getThemeColorForThreeJs } from 'lib/theme' import { Themes, getThemeColorForThreeJs } from 'lib/theme'
import { roundOff } from 'lib/utils'
export function profileStart({ export function profileStart({
from, from,
@ -101,7 +108,7 @@ export function straightSegment({
theme: Themes theme: Themes
isSelected?: boolean isSelected?: boolean
}): Group { }): Group {
const group = new Group() const segmentGroup = new Group()
const shape = new Shape() const shape = new Shape()
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale)
@ -133,7 +140,7 @@ export function straightSegment({
: STRAIGHT_SEGMENT_BODY : STRAIGHT_SEGMENT_BODY
mesh.name = STRAIGHT_SEGMENT_BODY mesh.name = STRAIGHT_SEGMENT_BODY
group.userData = { segmentGroup.userData = {
type: STRAIGHT_SEGMENT, type: STRAIGHT_SEGMENT,
id, id,
from, from,
@ -143,37 +150,60 @@ export function straightSegment({
callExpName, callExpName,
baseColor, baseColor,
} }
group.name = STRAIGHT_SEGMENT segmentGroup.name = STRAIGHT_SEGMENT
segmentGroup.add(mesh)
const length = Math.sqrt( const length = Math.sqrt(
Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2) Math.pow(to[0] - from[0], 2) + Math.pow(to[1] - from[1], 2)
) )
const arrowGroup = createArrowhead(scale, theme, color)
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 pxLength = length / scale
const shouldHide = pxLength < HIDE_SEGMENT_LENGTH const shouldHide = pxLength < HIDE_SEGMENT_LENGTH
arrowGroup.visible = !shouldHide
group.add(mesh)
if (callExpName !== 'close') group.add(arrowGroup)
// All segment types get an extra segment handle,
// Which is a little plus sign that appears at the origin of the segment
// and can be dragged to insert a new segment
const extraSegmentGroup = createExtraSegmentHandle(scale, texture, theme) const extraSegmentGroup = createExtraSegmentHandle(scale, texture, theme)
const offsetFromBase = new Vector2(to[0] - from[0], to[1] - from[1]) const directionVector = new Vector2(
.normalize() to[0] - from[0],
.multiplyScalar(EXTRA_SEGMENT_OFFSET_PX * scale) to[1] - from[1]
).normalize()
const offsetFromBase = directionVector.multiplyScalar(
EXTRA_SEGMENT_OFFSET_PX * scale
)
extraSegmentGroup.position.set( extraSegmentGroup.position.set(
from[0] + offsetFromBase.x, from[0] + offsetFromBase.x,
from[1] + offsetFromBase.y, from[1] + offsetFromBase.y,
0 0
) )
extraSegmentGroup.visible = !shouldHide extraSegmentGroup.visible = !shouldHide
group.add(extraSegmentGroup) segmentGroup.add(extraSegmentGroup)
return group // Segment decorators that only apply to non-close segments
if (callExpName !== 'close') {
// an arrowhead that appears at the end of the segment
const arrowGroup = createArrowhead(scale, theme, color)
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)
arrowGroup.visible = !shouldHide
segmentGroup.add(arrowGroup)
// A length indicator that appears at the midpoint of the segment
const lengthIndicatorGroup = createLengthIndicator({
from,
to,
scale,
length,
})
segmentGroup.add(lengthIndicatorGroup)
}
return segmentGroup
} }
function createArrowhead(scale = 1, theme: Themes, color?: number): Group { function createArrowhead(scale = 1, theme: Themes, color?: number): Group {
@ -230,6 +260,46 @@ function createExtraSegmentHandle(
return extraSegmentGroup return extraSegmentGroup
} }
/**
* Creates a group containing a CSS2DObject with the length of the segment
*/
function createLengthIndicator({
from,
to,
scale,
length,
}: {
from: Coords2d
to: Coords2d
scale: number
length: number
}) {
const lengthIndicatorGroup = new Group()
lengthIndicatorGroup.name = SEGMENT_LENGTH_LABEL
// Make the elements
const lengthIndicatorText = document.createElement('p')
lengthIndicatorText.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
lengthIndicatorText.innerText = roundOff(length).toString()
const lengthIndicatorWrapper = document.createElement('div')
// Style the elements
lengthIndicatorWrapper.style.position = 'absolute'
lengthIndicatorWrapper.appendChild(lengthIndicatorText)
const cssObject = new CSS2DObject(lengthIndicatorWrapper)
cssObject.name = SEGMENT_LENGTH_LABEL_TEXT
// Position the elements based on the line's heading
const offsetFromMidpoint = new Vector2(to[0] - from[0], to[1] - from[1])
.normalize()
.rotateAround(new Vector2(0, 0), -Math.PI / 2)
.multiplyScalar(SEGMENT_LENGTH_LABEL_OFFSET_PX * scale)
lengthIndicatorText.style.setProperty('--x', `${offsetFromMidpoint.x}px`)
lengthIndicatorText.style.setProperty('--y', `${offsetFromMidpoint.y}px`)
lengthIndicatorGroup.add(cssObject)
return lengthIndicatorGroup
}
export function tangentialArcToSegment({ export function tangentialArcToSegment({
prevSegment, prevSegment,
from, from,

View File

@ -254,6 +254,10 @@ code {
color: rgb(120, 120, 120, 0.8) !important; color: rgb(120, 120, 120, 0.8) !important;
} }
.segment-length-label-text {
transform: translate(var(--x, 0), var(--y, 0));
}
@layer components { @layer components {
kbd.hotkey { kbd.hotkey {
@apply font-mono text-xs inline-block px-1 py-0.5 rounded-sm; @apply font-mono text-xs inline-block px-1 py-0.5 rounded-sm;