1 Commits

Author SHA1 Message Date
Rune Harlyk d0b192a3e6 💅 Updates the frontpage 2025-03-08 13:06:56 +01:00
5 changed files with 802 additions and 806 deletions
+159 -164
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte'
import { import {
BufferGeometry, BufferGeometry,
Line, Line,
@@ -11,7 +11,7 @@
Vector3, Vector3,
type NormalBufferAttributes, type NormalBufferAttributes,
type Object3DEventMap type Object3DEventMap
} from 'three'; } from 'three'
import { import {
ModesEnum, ModesEnum,
kinematicData, kinematicData,
@@ -22,12 +22,12 @@
servoAngles, servoAngles,
mpu, mpu,
jointNames jointNames
} from '$lib/stores'; } from '$lib/stores'
import { footColor, populateModelCache, throttler, toeWorldPositions } from '$lib/utilities'; import { footColor, populateModelCache, throttler, toeWorldPositions } from '$lib/utilities'
import SceneBuilder from '$lib/sceneBuilder'; import SceneBuilder from '$lib/sceneBuilder'
import { lerp, degToRad } from 'three/src/math/MathUtils'; import { lerp, degToRad } from 'three/src/math/MathUtils'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import Kinematic, { type body_state_t } from '$lib/kinematic'; import Kinematic, { type body_state_t } from '$lib/kinematic'
import { import {
BezierState, BezierState,
CalibrationState, CalibrationState,
@@ -36,17 +36,18 @@
IdleState, IdleState,
RestState, RestState,
StandState StandState
} from '$lib/gait'; } from '$lib/gait'
import { radToDeg } from 'three/src/math/MathUtils.js'; import { radToDeg } from 'three/src/math/MathUtils.js'
import type { URDFRobot } from 'urdf-loader'; import type { URDFRobot } from 'urdf-loader'
import { get } from 'svelte/store'; import { get } from 'svelte/store'
interface Props { interface Props {
sky?: boolean; sky?: boolean
orbit?: boolean; orbit?: boolean
panel?: boolean; panel?: boolean
debug?: boolean; debug?: boolean
ground?: boolean; ground?: boolean
zoom?: number
} }
let { let {
@@ -54,24 +55,25 @@
orbit = false, orbit = false,
panel = true, panel = true,
debug = false, debug = false,
ground = true ground = true,
}: Props = $props(); zoom = 8
}: Props = $props()
let sceneManager = $state(new SceneBuilder()); let sceneManager = $state(new SceneBuilder())
let canvas: HTMLCanvasElement = $state(); let canvas: HTMLCanvasElement = $state()
let currentModelAngles: number[] = new Array(12).fill(0); let currentModelAngles: number[] = new Array(12).fill(0)
let modelTargetAngles: number[] = new Array(12).fill(0); let modelTargetAngles: number[] = new Array(12).fill(0)
let gui_panel: GUI; let gui_panel: GUI
let Throttler = new throttler(); let Throttler = new throttler()
let feet_trace = new Array(4).fill([]); let feet_trace = new Array(4).fill([])
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []; let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
let target: Object3D<Object3DEventMap>; let target: Object3D<Object3DEventMap>
let target_position = { x: 0, z: 0, yaw: 0 }; let target_position = { x: 0, z: 0, yaw: 0 }
let kinematic = new Kinematic(); let kinematic = new Kinematic()
let planners = { let planners = {
[ModesEnum.Deactivated]: new IdleState(), [ModesEnum.Deactivated]: new IdleState(),
@@ -81,10 +83,10 @@
[ModesEnum.Stand]: new StandState(), [ModesEnum.Stand]: new StandState(),
[ModesEnum.Crawl]: new EightPhaseWalkState(), [ModesEnum.Crawl]: new EightPhaseWalkState(),
[ModesEnum.Walk]: new BezierState() [ModesEnum.Walk]: new BezierState()
}; }
let lastTick = performance.now(); let lastTick = performance.now()
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]; const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
let body_state = { let body_state = {
omega: 0, omega: 0,
@@ -94,7 +96,7 @@
ym: 0.5, ym: 0.5,
zm: 0, zm: 0,
feet: planners[ModesEnum.Idle].default_feet_pos feet: planners[ModesEnum.Idle].default_feet_pos
}; }
let settings = { let settings = {
'Internal kinematic': true, 'Internal kinematic': true,
@@ -112,51 +114,51 @@
ym: 0.7, ym: 0.7,
zm: 0, zm: 0,
Background: 'black' Background: 'black'
}; }
onMount(async () => { onMount(async () => {
await populateModelCache(); await populateModelCache()
await createScene(); await createScene()
servoAngles.subscribe(updateAnglesFromStore); servoAngles.subscribe(updateAnglesFromStore)
if (panel) createPanel(); if (panel) createPanel()
}); })
onDestroy(() => { onDestroy(() => {
canvas.remove(); canvas.remove()
gui_panel?.destroy(); gui_panel?.destroy()
}); })
const updateAnglesFromStore = (angles: number[]) => { const updateAnglesFromStore = (angles: number[]) => {
if (sceneManager.isDragging) return; if (sceneManager.isDragging) return
if (settings['Internal kinematic']) return; if (settings['Internal kinematic']) return
modelTargetAngles = angles; modelTargetAngles = angles
}; }
const createPanel = () => { const createPanel = () => {
gui_panel = new GUI({ width: 310 }); gui_panel = new GUI({ width: 310 })
gui_panel.close(); gui_panel.close()
gui_panel.domElement.id = 'three-gui-panel'; gui_panel.domElement.id = 'three-gui-panel'
const general = gui_panel.addFolder('General'); const general = gui_panel.addFolder('General')
general.add(settings, 'Internal kinematic'); general.add(settings, 'Internal kinematic')
general.add(settings, 'Robot transform controls'); general.add(settings, 'Robot transform controls')
general.add(settings, 'Auto orient robot'); general.add(settings, 'Auto orient robot')
const kinematic = gui_panel.addFolder('Kinematics'); const kinematic = gui_panel.addFolder('Kinematics')
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen(); kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen(); kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen(); kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen(); kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen(); kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen(); kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen()
const visibility = gui_panel.addFolder('Visualization'); const visibility = gui_panel.addFolder('Visualization')
visibility.add(settings, 'Trace feet'); visibility.add(settings, 'Trace feet')
visibility.add(settings, 'Trace points', 1, 1000, 1); visibility.add(settings, 'Trace points', 1, 1000, 1)
visibility.add(settings, 'Target position'); visibility.add(settings, 'Target position')
visibility.add(settings, 'Smooth motion'); visibility.add(settings, 'Smooth motion')
visibility.addColor(settings, 'Background'); visibility.addColor(settings, 'Background')
}; }
const updateKinematicPosition = () => { const updateKinematicPosition = () => {
kinematicData.set([ kinematicData.set([
@@ -166,22 +168,19 @@
settings.xm, settings.xm,
settings.ym, settings.ym,
settings.zm settings.zm
]); ])
}; }
const updateAngles = (name: string, angle: number) => { const updateAngles = (name: string, angle: number) => {
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI); modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
Throttler.throttle( Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), }
100
);
};
const createScene = async () => { const createScene = async () => {
sceneManager sceneManager
.addRenderer({ antialias: true, canvas, alpha: true }) .addRenderer({ antialias: true, canvas, alpha: true })
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 }) .addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
.addOrbitControls(8, 30, orbit) .addOrbitControls(Math.min(zoom, 8), 30, orbit)
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 }) .addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
.addAmbientLight({ color: 0xffffff, intensity: 0.5 }) .addAmbientLight({ color: 0xffffff, intensity: 0.5 })
.addFogExp2(0xcccccc, 0.015) .addFogExp2(0xcccccc, 0.015)
@@ -189,46 +188,46 @@
.addTransformControls(sceneManager.model) .addTransformControls(sceneManager.model)
.fillParent() .fillParent()
.addRenderCb(render) .addRenderCb(render)
.startRenderLoop(); .startRenderLoop()
if (ground) sceneManager.addGroundPlane(); if (ground) sceneManager.addGroundPlane()
const geometry = new SphereGeometry(0.1, 32, 16); const geometry = new SphereGeometry(0.1, 32, 16)
const material = new MeshBasicMaterial({ color: 0xffff00 }); const material = new MeshBasicMaterial({ color: 0xffff00 })
target = new Mesh(geometry, material); target = new Mesh(geometry, material)
sceneManager.scene.add(target); sceneManager.scene.add(target)
if (debug) { if (debug) {
sceneManager.addDragControl(updateAngles); sceneManager.addDragControl(updateAngles)
} }
if (sky) sceneManager.addSky(); if (sky) sceneManager.addSky()
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const geometry = new BufferGeometry(); const geometry = new BufferGeometry()
const material = new LineBasicMaterial({ color: footColor() }); const material = new LineBasicMaterial({ color: footColor() })
const line = new Line(geometry, material); const line = new Line(geometry, material)
trace_lines.push(geometry); trace_lines.push(geometry)
sceneManager.scene.add(line); sceneManager.scene.add(line)
}
} }
};
const renderTraceLines = (foot_positions: Vector3[]) => { const renderTraceLines = (foot_positions: Vector3[]) => {
if (!settings['Trace feet']) { if (!settings['Trace feet']) {
if (!feet_trace.length) return; if (!feet_trace.length) return
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1))); trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
feet_trace = new Array(4).fill([]); feet_trace = new Array(4).fill([])
return; return
} }
trace_lines.forEach((line, i) => { trace_lines.forEach((line, i) => {
feet_trace[i].push(foot_positions[i]); feet_trace[i].push(foot_positions[i])
feet_trace[i] = feet_trace[i].slice(-settings['Trace points']); feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
line.setFromPoints(feet_trace[i]); line.setFromPoints(feet_trace[i])
}); })
}; }
const calculate_kinematics = () => { const calculate_kinematics = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return; if (sceneManager.isDragging || !settings['Internal kinematic']) return
const position: body_state_t = { const position: body_state_t = {
omega: settings.omega, omega: settings.omega,
phi: settings.phi, phi: settings.phi,
@@ -237,40 +236,36 @@
ym: settings.ym, ym: settings.ym,
zm: settings.zm, zm: settings.zm,
feet: body_state.feet feet: body_state.feet
}; }
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i])); let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
modelTargetAngles = new_angles; modelTargetAngles = new_angles
}; }
const orient_robot = (robot: URDFRobot, toes: Vector3[]) => { const orient_robot = (robot: URDFRobot, toes: Vector3[]) => {
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return; if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y)); robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y))
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1); robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1); robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
robot.rotation.z = smooth( robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1)
robot.rotation.z, robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1)
degToRad(-settings.phi + $mpu.heading + 90), robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1)
0.1 }
);
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1);
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1);
};
const update_camera = (robot: URDFRobot) => { const update_camera = (robot: URDFRobot) => {
if (!settings['Fix camera on robot']) return; if (!settings['Fix camera on robot']) return
sceneManager.orbit.target = robot.position.clone(); sceneManager.orbit.target = robot.position.clone()
}; }
const smooth = (start: number, end: number, amount: number) => { const smooth = (start: number, end: number, amount: number) => {
return settings['Smooth motion'] ? lerp(start, end, amount) : end; return settings['Smooth motion'] ? lerp(start, end, amount) : end
}; }
const update_gait = () => { const update_gait = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return; if (sceneManager.isDragging || !settings['Internal kinematic']) return
const controlData = get(outControllerData); const controlData = get(outControllerData)
const data = { const data = {
stop: controlData[0], stop: controlData[0],
lx: controlData[1], lx: controlData[1],
@@ -280,66 +275,66 @@
h: controlData[5], h: controlData[5],
s: controlData[6], s: controlData[6],
s1: controlData[7] s1: controlData[7]
}; }
body_state.ym = ((data.h + 127) * 0.75) / 100; body_state.ym = ((data.h + 127) * 0.75) / 100
let planner = planners[get(mode)]; let planner = planners[get(mode)]
const delta = performance.now() - lastTick; const delta = performance.now() - lastTick
lastTick = performance.now(); lastTick = performance.now()
body_state = planner.step(body_state, data, delta); body_state = planner.step(body_state, data, delta)
settings.omega = body_state.omega; settings.omega = body_state.omega
settings.phi = body_state.phi; settings.phi = body_state.phi
settings.psi = body_state.psi; settings.psi = body_state.psi
settings.xm = body_state.xm; settings.xm = body_state.xm
settings.ym = body_state.ym; settings.ym = body_state.ym
settings.zm = body_state.zm; settings.zm = body_state.zm
}; }
const update_robot_position = (robot: URDFRobot) => { const update_robot_position = (robot: URDFRobot) => {
if (!settings['Robot transform controls']) return; if (!settings['Robot transform controls']) return
settings.omega = radToDeg(robot.rotation.y); settings.omega = radToDeg(robot.rotation.y)
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90; settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90
settings.psi = radToDeg(robot.rotation.x) + 90; settings.psi = radToDeg(robot.rotation.x) + 90
settings.xm = robot.position.z * 100; settings.xm = robot.position.z * 100
settings.zm = -robot.position.x * 100; settings.zm = -robot.position.x * 100
}; }
const updateTargetPosition = () => { const updateTargetPosition = () => {
target.visible = settings['Target position']; target.visible = settings['Target position']
target.position.x = smooth(target.position.x, target_position.x, 0.5); target.position.x = smooth(target.position.x, target_position.x, 0.5)
target.position.z = smooth(target.position.z, target_position.z, 0.5); target.position.z = smooth(target.position.z, target_position.z, 0.5)
}; }
const render = () => { const render = () => {
const robot = sceneManager.model; const robot = sceneManager.model
if (!robot) return; if (!robot) return
const toes = toeWorldPositions(robot); const toes = toeWorldPositions(robot)
renderTraceLines(toes); renderTraceLines(toes)
update_camera(robot); update_camera(robot)
update_gait(); update_gait()
calculate_kinematics(); calculate_kinematics()
update_robot_position(robot); update_robot_position(robot)
sceneManager.transformControl.showX = settings['Robot transform controls']; sceneManager.transformControl.showX = settings['Robot transform controls']
sceneManager.transformControl.showY = settings['Robot transform controls']; sceneManager.transformControl.showY = settings['Robot transform controls']
sceneManager.transformControl.showZ = settings['Robot transform controls']; sceneManager.transformControl.showZ = settings['Robot transform controls']
for (let i = 0; i < $jointNames.length; i++) { for (let i = 0; i < $jointNames.length; i++) {
currentModelAngles[i] = smooth( currentModelAngles[i] = smooth(
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI), (robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
modelTargetAngles[i], modelTargetAngles[i],
0.1 0.1
); )
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i])); robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]))
} }
orient_robot(robot, toes); orient_robot(robot, toes)
updateTargetPosition(); updateTargetPosition()
}; }
</script> </script>
<svelte:window onresize={sceneManager.fillParent} /> <svelte:window onresize={sceneManager.fillParent} />
+207 -207
View File
@@ -21,78 +21,78 @@ import {
Group, Group,
MeshBasicMaterial, MeshBasicMaterial,
RepeatWrapping RepeatWrapping
} from 'three'; } from 'three'
import { Sky } from 'three/addons/objects/Sky.js'; import { Sky } from 'three/addons/objects/Sky.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
import { Reflector } from 'three/examples/jsm/objects/Reflector.js'; import { Reflector } from 'three/examples/jsm/objects/Reflector.js'
import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'; import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'; import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'
import { sunCalculator } from './utilities/position-utilities'; import { sunCalculator } from './utilities/position-utilities'
export const addScene = () => new Scene(); export const addScene = () => new Scene()
interface position { interface position {
x?: number; x?: number
y?: number; y?: number
z?: number; z?: number
} }
interface light { interface light {
color?: ColorRepresentation; color?: ColorRepresentation
intensity?: number; intensity?: number
} }
interface arrowOptions { interface arrowOptions {
origin: position; origin: position
direction: position; direction: position
length?: number; length?: number
color?: ColorRepresentation; color?: ColorRepresentation
} }
type directionalLight = position & light; type directionalLight = position & light
export default class SceneBuilder { export default class SceneBuilder {
public scene: Scene; public scene: Scene
public camera!: PerspectiveCamera; public camera!: PerspectiveCamera
public ground!: Mesh; public ground!: Mesh
public renderer!: WebGLRenderer; public renderer!: WebGLRenderer
public orbit: OrbitControls; public orbit: OrbitControls
public callback: Function | undefined; public callback: Function | undefined
public gridHelper!: GridHelper; public gridHelper!: GridHelper
public model!: URDFRobot; public model!: URDFRobot
public liveStreamTexture!: CanvasTexture; public liveStreamTexture!: CanvasTexture
private fog!: FogExp2; private fog!: FogExp2
private isLoaded: boolean = false; private isLoaded: boolean = false
public isDragging: boolean = false; public isDragging: boolean = false
highlightMaterial: any; highlightMaterial: any
sky!: Sky; sky!: Sky
transformControl: TransformControls; transformControl: TransformControls
public modelGroup!: Group; public modelGroup!: Group
constructor() { constructor() {
this.scene = new Scene(); this.scene = new Scene()
if (this.scene.environment?.mapping) { if (this.scene.environment?.mapping) {
this.scene.environment.mapping = EquirectangularReflectionMapping; this.scene.environment.mapping = EquirectangularReflectionMapping
} }
return this; return this
} }
public addRenderer = (parameters?: WebGLRendererParameters) => { public addRenderer = (parameters?: WebGLRendererParameters) => {
this.renderer = new WebGLRenderer(parameters); this.renderer = new WebGLRenderer(parameters)
this.renderer.outputColorSpace = 'srgb'; this.renderer.outputColorSpace = 'srgb'
this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.enabled = true
this.renderer.shadowMap.type = PCFSoftShadowMap; this.renderer.shadowMap.type = PCFSoftShadowMap
this.renderer.toneMapping = ACESFilmicToneMapping; this.renderer.toneMapping = ACESFilmicToneMapping
this.renderer.toneMappingExposure = 0.85; this.renderer.toneMappingExposure = 0.85
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement); if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement)
return this; return this
}; }
public addSky = () => { public addSky = () => {
this.sky = new Sky(); this.sky = new Sky()
this.sky.scale.setScalar(450000); this.sky.scale.setScalar(450000)
this.scene.add(this.sky); this.scene.add(this.sky)
const effectController = { const effectController = {
turbidity: 10, turbidity: 10,
rayleigh: 3, rayleigh: 3,
@@ -101,279 +101,279 @@ export default class SceneBuilder {
elevation: sunCalculator.calculateSunElevation(), elevation: sunCalculator.calculateSunElevation(),
azimuth: 200, azimuth: 200,
exposure: this.renderer.toneMappingExposure exposure: this.renderer.toneMappingExposure
}; }
const uniforms = this.sky.material.uniforms; const uniforms = this.sky.material.uniforms
uniforms['turbidity'].value = effectController.turbidity; uniforms['turbidity'].value = effectController.turbidity
uniforms['rayleigh'].value = effectController.rayleigh; uniforms['rayleigh'].value = effectController.rayleigh
uniforms['mieCoefficient'].value = effectController.mieCoefficient; uniforms['mieCoefficient'].value = effectController.mieCoefficient
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG; uniforms['mieDirectionalG'].value = effectController.mieDirectionalG
this.renderer.toneMappingExposure = 0.5; this.renderer.toneMappingExposure = 0.5
const phi = MathUtils.degToRad(90 - effectController.elevation); const phi = MathUtils.degToRad(90 - effectController.elevation)
const theta = MathUtils.degToRad(effectController.azimuth); const theta = MathUtils.degToRad(effectController.azimuth)
const sun = new Vector3(); const sun = new Vector3()
sun.setFromSphericalCoords(1, phi, theta); sun.setFromSphericalCoords(1, phi, theta)
uniforms['sunPosition'].value.copy(sun); uniforms['sunPosition'].value.copy(sun)
return this; return this
}; }
public addPerspectiveCamera = (options: position) => { public addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera(); this.camera = new PerspectiveCamera()
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0); this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0)
this.scene.add(this.camera); this.scene.add(this.camera)
return this; return this
}; }
public addGroundPlane = (options?: position) => { public addGroundPlane = (options?: position) => {
const checkerboardTexture = this.createCheckerboardTexture(1024, 2); const checkerboardTexture = this.createCheckerboardTexture(1024, 2)
checkerboardTexture.wrapS = RepeatWrapping; checkerboardTexture.wrapS = RepeatWrapping
checkerboardTexture.wrapT = RepeatWrapping; checkerboardTexture.wrapT = RepeatWrapping
checkerboardTexture.repeat.set(100, 100); checkerboardTexture.repeat.set(100, 100)
const checkerboardMat = new MeshBasicMaterial({ const checkerboardMat = new MeshBasicMaterial({
map: checkerboardTexture, map: checkerboardTexture,
opacity: 0.1, opacity: 0.1,
transparent: true transparent: true
}); })
const plane = new PlaneGeometry(400, 400); const plane = new PlaneGeometry(400, 400)
this.ground = new Mesh(plane, checkerboardMat); this.ground = new Mesh(plane, checkerboardMat)
this.ground.rotation.x = -Math.PI / 2; this.ground.rotation.x = -Math.PI / 2
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0); this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0)
this.ground.receiveShadow = true; this.ground.receiveShadow = true
this.scene.add(this.ground); this.scene.add(this.ground)
const mirror = new Reflector(plane, { const mirror = new Reflector(plane, {
clipBias: 0.003, clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio, textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio, textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0x00bfff color: 0x00bfff
}); })
mirror.rotateX(-Math.PI / 2); mirror.rotateX(-Math.PI / 2)
this.scene.add(mirror); this.scene.add(mirror)
return this; return this
}; }
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => { public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
this.orbit = new OrbitControls(this.camera, this.renderer.domElement); this.orbit = new OrbitControls(this.camera, this.renderer.domElement)
this.orbit.minDistance = minDistance; this.orbit.minDistance = 5
this.orbit.maxDistance = maxDistance; this.orbit.maxDistance = maxDistance
this.orbit.autoRotate = autoRotate; this.orbit.autoRotate = autoRotate
this.orbit.update(); this.orbit.update()
return this; return this
}; }
public addAmbientLight = (options: light) => { public addAmbientLight = (options: light) => {
const ambientLight = new AmbientLight(options.color, options.intensity); const ambientLight = new AmbientLight(options.color, options.intensity)
this.scene.add(ambientLight); this.scene.add(ambientLight)
return this; return this
}; }
public addDirectionalLight = (options: directionalLight) => { public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity); const directionalLight = new DirectionalLight(options.color, options.intensity)
directionalLight.castShadow = true; directionalLight.castShadow = true
directionalLight.shadow.camera.top = 10; directionalLight.shadow.camera.top = 10
directionalLight.shadow.camera.bottom = -10; directionalLight.shadow.camera.bottom = -10
directionalLight.shadow.camera.right = 10; directionalLight.shadow.camera.right = 10
directionalLight.shadow.camera.left = -10; directionalLight.shadow.camera.left = -10
directionalLight.shadow.mapSize.set(4096, 4096); directionalLight.shadow.mapSize.set(4096, 4096)
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0)
this.scene.add(directionalLight); this.scene.add(directionalLight)
return this; return this
}; }
private createCheckerboardTexture = (size: number, squares: number) => { private createCheckerboardTexture = (size: number, squares: number) => {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas')
canvas.width = size; canvas.width = size
canvas.height = size; canvas.height = size
const context = canvas.getContext('2d'); const context = canvas.getContext('2d')
const squareSize = size / squares; const squareSize = size / squares
for (let y = 0; y < squares; y++) { for (let y = 0; y < squares; y++) {
for (let x = 0; x < squares; x++) { for (let x = 0; x < squares; x++) {
context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000'; context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000'
context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize); context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize)
} }
} }
const texture = new CanvasTexture(canvas); const texture = new CanvasTexture(canvas)
texture.wrapS = texture.wrapT = RepeatWrapping; texture.wrapS = texture.wrapT = RepeatWrapping
texture.anisotropy = 16; texture.anisotropy = 16
return texture; return texture
}; }
public addFogExp2 = (color: ColorRepresentation, density?: number) => { public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.scene.fog = new FogExp2(color, density); this.scene.fog = new FogExp2(color, density)
return this; return this
}; }
public fillParent = () => { public fillParent = () => {
const parentElement = this.renderer.domElement.parentElement; const parentElement = this.renderer.domElement.parentElement
if (parentElement) { if (parentElement) {
const width = parentElement.clientWidth; const width = parentElement.clientWidth
const height = parentElement.clientHeight; const height = parentElement.clientHeight
this.handleResize(width, height); this.handleResize(width, height)
}
return this
} }
return this;
};
public handleResize = (width = window.innerWidth, height = window.innerHeight) => { public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
this.renderer.setSize(width, height); this.renderer.setSize(width, height)
this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setPixelRatio(window.devicePixelRatio)
this.camera.aspect = width / height; this.camera.aspect = width / height
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix()
return this; return this
}; }
public addRenderCb = (callback: Function) => { public addRenderCb = (callback: Function) => {
this.callback = callback; this.callback = callback
return this; return this
}; }
public startRenderLoop = () => { public startRenderLoop = () => {
this.renderer.setAnimationLoop(() => { this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera)
this.orbit.update(); this.orbit.update()
this.handleRobotShadow(); this.handleRobotShadow()
if (this.callback) this.callback(); if (this.callback) this.callback()
if (!this.liveStreamTexture) return; if (!this.liveStreamTexture) return
}); })
return this; return this
}; }
public addArrowHelper = (options?: arrowOptions) => { public addArrowHelper = (options?: arrowOptions) => {
const dir = new Vector3( const dir = new Vector3(
options?.direction.x ?? 0, options?.direction.x ?? 0,
options?.direction.y ?? 0, options?.direction.y ?? 0,
options?.direction.z ?? 0 options?.direction.z ?? 0
); )
const origin = new Vector3( const origin = new Vector3(
options?.origin.x ?? 0, options?.origin.x ?? 0,
options?.origin.y ?? 0, options?.origin.y ?? 0,
options?.origin.z ?? 0 options?.origin.z ?? 0
); )
const arrowHelper = new ArrowHelper( const arrowHelper = new ArrowHelper(
dir, dir,
origin, origin,
options?.length ?? 1.5, options?.length ?? 1.5,
options?.color ?? 0xff0000 options?.color ?? 0xff0000
); )
this.scene.add(arrowHelper); this.scene.add(arrowHelper)
return this; return this
};
private setJointValue(jointName: string, angle: number) {
if (!this.model) return;
if (!this.model.joints[jointName]) return;
this.model.joints[jointName].setJointValue(angle);
} }
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'; private setJointValue(jointName: string, angle: number) {
if (!this.model) return
if (!this.model.joints[jointName]) return
this.model.joints[jointName].setJointValue(angle)
}
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => { highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
const traverse = (c: any) => { const traverse = (c: any) => {
if (c.type === 'Mesh') { if (c.type === 'Mesh') {
if (revert) { if (revert) {
c.material = c.__origMaterial; c.material = c.__origMaterial
delete c.__origMaterial; delete c.__origMaterial
} else { } else {
c.__origMaterial = c.material; c.__origMaterial = c.material
c.material = material; c.material = material
} }
} }
if (c === m || !this.isJoint(c)) { if (c === m || !this.isJoint(c)) {
for (let i = 0; i < c.children.length; i++) { for (let i = 0; i < c.children.length; i++) {
const child = c.children[i]; const child = c.children[i]
if (!child.isURDFCollider) { if (!child.isURDFCollider) {
traverse(c.children[i]); traverse(c.children[i])
} }
} }
} }
}; }
traverse(m); traverse(m)
}; }
public addTransformControls = (model: any) => { public addTransformControls = (model: any) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement); this.transformControl = new TransformControls(this.camera, this.renderer.domElement)
this.transformControl.addEventListener('dragging-changed', (event: any) => { this.transformControl.addEventListener('dragging-changed', (event: any) => {
this.orbit.enabled = !event.value; this.orbit.enabled = !event.value
this.isDragging = !event.value; this.isDragging = !event.value
}); })
this.transformControl.attach(model); this.transformControl.attach(model)
this.scene.add(this.transformControl); this.scene.add(this.transformControl)
this.transformControl.setMode('rotate'); this.transformControl.setMode('rotate')
return this; return this
}; }
public addModel = (model: any) => { public addModel = (model: any) => {
this.modelGroup = new Group(); this.modelGroup = new Group()
this.modelGroup.add(model); this.modelGroup.add(model)
this.model = model; this.model = model
this.scene.add(this.modelGroup); this.scene.add(this.modelGroup)
return this; return this
}; }
public addDragControl = (updateAngle: any) => { public addDragControl = (updateAngle: any) => {
const highlightColor = '#FFFFFF'; const highlightColor = '#FFFFFF'
const highlightMaterial = new MeshPhongMaterial({ const highlightMaterial = new MeshPhongMaterial({
shininess: 10, shininess: 10,
color: highlightColor, color: highlightColor,
emissive: highlightColor, emissive: highlightColor,
emissiveIntensity: 0.9 emissiveIntensity: 0.9
}); })
const dragControls = new PointerURDFDragControls( const dragControls = new PointerURDFDragControls(
this.scene, this.scene,
this.camera, this.camera,
this.renderer.domElement this.renderer.domElement
); )
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
this.setJointValue(joint.name, angle); this.setJointValue(joint.name, angle)
updateAngle(joint.name, angle); updateAngle(joint.name, angle)
}; }
dragControls.onDragStart = () => { dragControls.onDragStart = () => {
this.orbit.enabled = false; this.orbit.enabled = false
this.isDragging = true; this.isDragging = true
}; }
dragControls.onDragEnd = () => { dragControls.onDragEnd = () => {
this.orbit.enabled = true; this.orbit.enabled = true
this.isDragging = false; this.isDragging = false
}; }
dragControls.onHover = (joint: URDFMimicJoint) => dragControls.onHover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, false, highlightMaterial); this.highlightLinkGeometry(joint, false, highlightMaterial)
dragControls.onUnhover = (joint: URDFMimicJoint) => dragControls.onUnhover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, true, highlightMaterial); this.highlightLinkGeometry(joint, true, highlightMaterial)
this.renderer.domElement.addEventListener( this.renderer.domElement.addEventListener(
'touchstart', 'touchstart',
data => dragControls._mouseDown(data.touches[0]), data => dragControls._mouseDown(data.touches[0]),
{ passive: true } { passive: true }
); )
this.renderer.domElement.addEventListener( this.renderer.domElement.addEventListener(
'touchmove', 'touchmove',
data => dragControls._mouseMove(data.touches[0]), data => dragControls._mouseMove(data.touches[0]),
{ passive: true } { passive: true }
); )
this.renderer.domElement.addEventListener( this.renderer.domElement.addEventListener(
'touchend', 'touchend',
data => dragControls._mouseUp(data.touches[0]), data => dragControls._mouseUp(data.touches[0]),
{ passive: true } { passive: true }
); )
return this; return this
}; }
public toggleFog = () => { public toggleFog = () => {
this.scene.fog = this.scene.fog ? null : this.fog; this.scene.fog = this.scene.fog ? null : this.fog
}; }
private handleRobotShadow = () => { private handleRobotShadow = () => {
if (this.isLoaded) return; if (this.isLoaded) return
const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10); const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10)
setTimeout(() => clearInterval(intervalId), 1000); setTimeout(() => clearInterval(intervalId), 1000)
this.isLoaded = true; this.isLoaded = true
}; }
} }
+55 -55
View File
@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte'
import { page } from '$app/state'; import { page } from '$app/state'
import { Modals, modals } from 'svelte-modals'; import { Modals, modals } from 'svelte-modals'
import Toast from '$lib/components/toasts/Toast.svelte'; import Toast from '$lib/components/toasts/Toast.svelte'
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications'
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition'
import '../app.css'; import '../app.css'
import Menu from '../lib/components/menu/Menu.svelte'; import Menu from '../lib/components/menu/Menu.svelte'
import Statusbar from '../lib/components/statusbar/statusbar.svelte'; import Statusbar from '../lib/components/statusbar/statusbar.svelte'
import { import {
telemetry, telemetry,
analytics, analytics,
@@ -20,81 +20,81 @@
socket, socket,
location, location,
useFeatureFlags useFeatureFlags
} from '$lib/stores'; } from '$lib/stores'
import type { Analytics, DownloadOTA } from '$lib/types/models'; import type { Analytics, DownloadOTA } from '$lib/types/models'
interface Props { interface Props {
children?: import('svelte').Snippet; children?: import('svelte').Snippet
} }
let { children }: Props = $props(); let { children }: Props = $props()
const features = useFeatureFlags(); const features = useFeatureFlags()
onMount(async () => { onMount(async () => {
const ws = $location ? $location : window.location.host; const ws = $location ? $location : window.location.host
socket.init(`ws://${ws}/api/ws/events`); socket.init(`ws://${ws}/api/ws/events`)
addEventListeners(); addEventListeners()
outControllerData.subscribe(data => socket.sendEvent('input', { data })); outControllerData.subscribe(data => socket.sendEvent('input', { data }))
mode.subscribe(data => socket.sendEvent('mode', { data })); mode.subscribe(data => socket.sendEvent('mode', { data }))
servoAnglesOut.subscribe(data => socket.sendEvent('angles', { data })); servoAnglesOut.subscribe(data => socket.sendEvent('angles', { data }))
kinematicData.subscribe(data => socket.sendEvent('position', { data })); kinematicData.subscribe(data => socket.sendEvent('position', { data }))
}); })
onDestroy(() => { onDestroy(() => {
removeEventListeners(); removeEventListeners()
}); })
const addEventListeners = () => { const addEventListeners = () => {
socket.on('open', handleOpen); socket.on('open', handleOpen)
socket.on('close', handleClose); socket.on('close', handleClose)
socket.on('error', handleError); socket.on('error', handleError)
socket.on('rssi', handleNetworkStatus); socket.on('rssi', handleNetworkStatus)
socket.on('mode', (data: ModesEnum) => mode.set(data)); socket.on('mode', (data: ModesEnum) => mode.set(data))
socket.on('analytics', handleAnalytics); socket.on('analytics', handleAnalytics)
socket.on('angles', (angles: number[]) => { socket.on('angles', (angles: number[]) => {
if (angles.length) servoAngles.set(angles); if (angles.length) servoAngles.set(angles)
}); })
features.subscribe(data => { features.subscribe(data => {
if (data?.download_firmware) socket.on('otastatus', handleOAT); if (data?.download_firmware) socket.on('otastatus', handleOAT)
if (data?.sonar) socket.on('sonar', data => console.log(data)); if (data?.sonar) socket.on('sonar', data => console.log(data))
}); })
}; }
const removeEventListeners = () => { const removeEventListeners = () => {
socket.off('analytics', handleAnalytics); socket.off('analytics', handleAnalytics)
socket.off('open', handleOpen); socket.off('open', handleOpen)
socket.off('close', handleClose); socket.off('close', handleClose)
socket.off('rssi', handleNetworkStatus); socket.off('rssi', handleNetworkStatus)
socket.off('otastatus', handleOAT); socket.off('otastatus', handleOAT)
}; }
const handleOpen = () => { const handleOpen = () => {
notifications.success('Connection to device established', 5000); notifications.success('Connection to device established', 5000)
}; }
const handleClose = () => { const handleClose = () => {
notifications.error('Connection to device lost', 5000); notifications.error('Connection to device lost', 5000)
telemetry.setRSSI(0); telemetry.setRSSI(0)
}; }
const handleError = (data: any) => console.error(data); const handleError = (data: any) => console.error(data)
const handleAnalytics = (data: Analytics) => analytics.addData(data); const handleAnalytics = (data: Analytics) => analytics.addData(data)
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data); const handleNetworkStatus = (data: number) => telemetry.setRSSI(data)
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data); const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data)
let menuOpen = $state(false); let menuOpen = $state(false)
</script> </script>
<svelte:head> <svelte:head>
<title>{page.data.title}</title> <title>{page.data.title}</title>
</svelte:head> </svelte:head>
<div class="drawer"> <div class="drawer h-screen">
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} /> <input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
<div class="drawer-content flex flex-col"> <div class="drawer-content flex flex-col">
<!-- Status bar content here --> <!-- Status bar content here -->
@@ -117,8 +117,8 @@
<div <div
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm" class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
transition:fade transition:fade
onclick={modals.closeAll} onclick={modals.closeAll}>
></div> </div>
{/snippet} {/snippet}
</Modals> </Modals>
+20 -19
View File
@@ -1,28 +1,29 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import { goto } from '$app/navigation'
import { notifications } from '$lib/components/toasts/notifications'; import Visualization from '$lib/components/Visualization.svelte'
import Visualization from '$lib/components/Visualization.svelte'; import { socket } from '$lib/stores'
import { onMount } from 'svelte'
interface Props { onMount(() => {
data: PageData; socket.subscribe(isConnected => {
if (isConnected) {
goto('/controller')
} }
})
let { data }: Props = $props(); })
</script> </script>
<div class="hero bg-base-100 h-screen"> <div class="w-full h-full flex justify-center items-center">
<div class="card md:card-side bg-base-200 shadow-2xl flex justify-center items-center"> <div class="h-full flex flex-col">
<div class="w-64 h-64"> <div class="grow-3 w-80 relative">
<Visualization sky={false} orbit panel={false} ground={false}/> <Visualization sky={false} orbit panel={false} ground={false} zoom={8} />
<div class="absolute bottom-0 w-full h-40 bg-gradient-to-t from-base-100 to-transparent">
</div> </div>
<div class="card-body w-80"> </div>
<h2 class="card-title text-center text-2xl">Welcome to {data.app_name}</h2> <div class="grow-3 flex justify-center">
<p class="py-6 text-center"></p> <a class="btn btn-primary rounded-full" href={$socket ? '/controller' : '/connection'}>
<a Add Robot Dog
class="btn btn-primary" </a>
href="/controller"
onclick={() => notifications.success('You did it!', 1000)}>Begin</a
>
</div> </div>
</div> </div>
</div> </div>