🌌 Migrate app to svelte-5

This commit is contained in:
Rune Harlyk
2025-02-26 22:28:30 +01:00
committed by Rune Harlyk
parent d9285bbdc0
commit 788f4ffea3
51 changed files with 1512 additions and 1348 deletions
+38 -37
View File
@@ -1,43 +1,44 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { createEventDispatcher } from 'svelte';
import { Down } from './icons';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { Down } from './icons';
const dispatch = createEventDispatcher();
function openCollapsible() {
open = !open;
if (open) {
opened();
} else {
closed();
}
}
function openCollapsible() {
open = !open;
if (open) {
dispatch('opened');
} else {
dispatch('closed');
}
}
export let open = false;
let { icon, title, children, open, opened, closed, class: klass } = $props();
</script>
<div class="{$$props.class || ''} relative grid w-full max-w-2xl self-center overflow-hidden">
<div class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
<span class="inline-flex items-baseline">
<slot name="icon" />
<slot name="title" />
</span>
<button class="btn btn-circle btn-ghost btn-sm" on:click={() => openCollapsible()}>
<Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open
? 'rotate-180'
: ''}"
/>
</button>
</div>
{#if open}
<div
class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<slot />
</div>
{/if}
<div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden">
<div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
>
<span class="inline-flex items-baseline">
{@render icon?.()}
{@render title?.()}
</span>
<button class="btn btn-circle btn-ghost btn-sm" onclick={() => openCollapsible()}>
<Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
open
) ?
'rotate-180'
: ''}"
/>
</button>
</div>
{#if open}
<div
class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
{@render children?.()}
</div>
{/if}
</div>
+28 -14
View File
@@ -1,47 +1,61 @@
<script lang="ts">
import { createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import { closeModal } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { Cancel, Check } from '$lib/components/icons';
// provided by <Modals />
export let isOpen: boolean;
export let title: string;
export let message: string;
export let onConfirm: any;
export let labels = {
interface Props {
isOpen: boolean;
title: string;
message: string;
onConfirm: any;
labels?: any;
}
let {
isOpen,
title,
message,
onConfirm,
labels = {
cancel: { label: 'Cancel', icon: Cancel },
confirm: { label: 'OK', icon: Check }
};
}
}: Props = $props();
</script>
{#if isOpen}
{@const SvelteComponent = labels?.confirm.icon}
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
on:introstart
on:outroend
onintrostart={bubble('introstart')}
onoutroend={bubble('outroend')}
use:focusTrap
>
<div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2" />
<div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2" />
<div class="divider my-2"></div>
<div class="flex justify-end gap-2">
<button class="btn btn-primary inline-flex items-center" on:click={closeModal}
><svelte:component this={labels.cancel.icon} class="mr-2 h-5 w-5" /><span
<button class="btn btn-primary inline-flex items-center" onclick={closeModal}
><labels.cancel.icon class="mr-2 h-5 w-5" /><span
>{labels?.cancel.label}</span
></button
>
<button
class="btn btn-warning text-warning-content inline-flex items-center"
on:click={onConfirm}
><svelte:component this={labels?.confirm.icon} class="mr-2 h-5 w-5" /><span
onclick={onConfirm}
><SvelteComponent class="mr-2 h-5 w-5" /><span
>{labels?.confirm.label}</span
></button
>
@@ -1,4 +1,7 @@
<script lang="ts">
import { run, createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import { closeAllModals, onBeforeClose } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
@@ -6,35 +9,45 @@
import { Cancel } from './icons';
// provided by <Modals />
export let isOpen: boolean;
let updating = true;
let progress = 0;
$: if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress;
interface Props {
isOpen: boolean;
}
$: if ($telemetry.download_ota.status == 'error') {
updating = false;
}
let { isOpen }: Props = $props();
let message = 'Preparing ...';
let timerId: number;
let updating = $state(true);
$: if ($telemetry.download_ota.status == 'progress') {
message = 'Downloading ...';
} else if ($telemetry.download_ota.status == 'error') {
message = $telemetry.download_ota.error;
} else if ($telemetry.download_ota.status == 'finished') {
message = 'Restarting ...';
progress = 0;
// Reload page after 5 sec
timerId = setTimeout(() => {
closeAllModals();
location.reload();
}, 5000);
}
let progress = $state(0);
run(() => {
if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress;
}
});
run(() => {
if ($telemetry.download_ota.status == 'error') {
updating = false;
}
});
let message = $state('Preparing ...');
let timerId: number = $state();
run(() => {
if ($telemetry.download_ota.status == 'progress') {
message = 'Downloading ...';
} else if ($telemetry.download_ota.status == 'error') {
message = $telemetry.download_ota.error;
} else if ($telemetry.download_ota.status == 'finished') {
message = 'Restarting ...';
progress = 0;
// Reload page after 5 sec
timerId = setTimeout(() => {
closeAllModals();
location.reload();
}, 5000);
}
});
onBeforeClose(() => {
if (updating) {
@@ -54,32 +67,32 @@
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
on:introstart
on:outroend
onintrostart={bubble('introstart')}
onoutroend={bubble('outroend')}
use:focusTrap
>
<div
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">Updating Firmware</h2>
<div class="divider my-2" />
<div class="divider my-2"></div>
<div class="overflow-y-auto">
<div class="bg-base-100 flex flex-col items-center justify-center p-6">
{#if $telemetry.download_ota.status == 'progress'}
<progress class="progress progress-primary w-56" value={progress} max="100" />
<progress class="progress progress-primary w-56" value={progress} max="100"></progress>
{:else}
<progress class="progress progress-primary w-56" />
<progress class="progress progress-primary w-56"></progress>
{/if}
<p class="mt-8 text-2xl">{message}</p>
</div>
</div>
<div class="divider my-2" />
<div class="divider my-2"></div>
<div class="flex flex-wrap justify-end gap-2">
<div class="flex-grow" />
<div class="flex-grow"></div>
<button
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
disabled={updating}
on:click={() => {
onclick={() => {
closeAllModals();
location.reload();
}}
+24 -11
View File
@@ -1,15 +1,28 @@
<script lang="ts">
import { createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { Check } from './icons';
// provided by <Modals />
export let isOpen: boolean;
export let title: string;
export let message: string;
export let onDismiss: any;
export let dismiss = { label: 'Dismiss', icon: Check };
interface Props {
isOpen: boolean;
title: string;
message: string;
onDismiss: any;
dismiss?: any;
}
let {
isOpen,
title,
message,
onDismiss,
dismiss = { label: 'Dismiss', icon: Check }
}: Props = $props();
</script>
{#if isOpen}
@@ -17,22 +30,22 @@
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
on:introstart
on:outroend
onintrostart={bubble('introstart')}
onoutroend={bubble('outroend')}
use:focusTrap
>
<div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2" />
<div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2" />
<div class="divider my-2"></div>
<div class="flex justify-end gap-2">
<button
class="btn btn-warning text-warning-content inline-flex items-center"
on:click={onDismiss}
><svelte:component this={dismiss.icon} class="mr-2 h-5 w-5" /><span>{dismiss.label}</span
onclick={onDismiss}
><dismiss.icon class="mr-2 h-5 w-5" /><span>{dismiss.label}</span
></button
>
</div>
+22 -9
View File
@@ -2,8 +2,21 @@
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { Down } from './icons';
export let open = true;
export let collapsible = true;
interface Props {
open?: boolean;
collapsible?: boolean;
icon?: import('svelte').Snippet;
title?: import('svelte').Snippet;
children?: import('svelte').Snippet;
}
let {
open = $bindable(true),
collapsible = true,
icon,
title,
children
}: Props = $props();
</script>
{#if collapsible}
@@ -14,12 +27,12 @@
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
>
<span class="inline-flex items-baseline">
<slot name="icon" />
<slot name="title" />
{@render icon?.()}
{@render title?.()}
</span>
<button
class="btn btn-circle btn-ghost btn-sm"
on:click={() => {
onclick={() => {
open = !open;
}}
>
@@ -35,7 +48,7 @@
class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<slot />
{@render children?.()}
</div>
{/if}
</div>
@@ -45,12 +58,12 @@
>
<div class="min-h-16 w-full p-4 text-xl font-medium">
<span class="inline-flex items-baseline">
<slot name="icon" />
<slot name="title" />
{@render icon?.()}
{@render title?.()}
</span>
</div>
<div class="flex flex-col gap-2 p-4 pt-0">
<slot />
{@render children?.()}
</div>
</div>
{/if}
+1 -1
View File
@@ -2,7 +2,7 @@
import { onDestroy } from 'svelte';
import { location } from '$lib/stores';
let source = `${$location}/api/camera/stream`;
let source = $state(`${$location}/api/camera/stream`);
onDestroy(() => (source = '#'));
</script>
+7 -6
View File
@@ -4,30 +4,31 @@
import { notifications } from '$lib/components/toasts/notifications';
import { error, info, success, warning } from './icons';
export let theme = {
/** @type {{theme?: any, icon?: any}} */
let { theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
};
export let icon = {
}, icon = {
error: error,
success: success,
warning: warning,
info: info
};
} } = $props();
</script>
<div class="toast toast-end mr-4">
{#each $notifications as notification (notification.id)}
{@const SvelteComponent = icon[notification.type]}
<div
animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }}
>
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
<SvelteComponent class="h-6 w-6 flex-shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
+18 -8
View File
@@ -41,14 +41,24 @@
import type { URDFRobot } from 'urdf-loader';
import { get } from 'svelte/store';
export let sky = true;
export let orbit = false;
export let panel = true;
export let debug = false;
export let ground = true;
interface Props {
sky?: boolean;
orbit?: boolean;
panel?: boolean;
debug?: boolean;
ground?: boolean;
}
let sceneManager = new SceneBuilder();
let canvas: HTMLCanvasElement;
let {
sky = true,
orbit = false,
panel = true,
debug = false,
ground = true
}: Props = $props();
let sceneManager = $state(new SceneBuilder());
let canvas: HTMLCanvasElement = $state();
let currentModelAngles: number[] = new Array(12).fill(0);
let modelTargetAngles: number[] = new Array(12).fill(0);
@@ -332,6 +342,6 @@
};
</script>
<svelte:window on:resize={sceneManager.fillParent} />
<svelte:window onresize={sceneManager.fillParent} />
<canvas bind:this={canvas}></canvas>
@@ -1,11 +1,15 @@
<script lang="ts">
import { MdiEyeOffOutline, MdiEyeOutline } from "../icons";
export let show = false;
export let value = '';
export let id = '';
interface Props {
show?: boolean;
value?: string;
id?: string;
}
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props();
$: type = show ? 'text' : 'password';
let type = $derived(show ? 'text' : 'password');
const handleInput = (e: any) => value = e.target.value
@@ -13,9 +17,9 @@
</script>
<label class="input input-bordered flex items-center gap-2">
<input {type} class="grow" {value} on:input={handleInput} {id} />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={togglePassword} role="button" tabindex="0">
<input {type} class="grow" {value} oninput={handleInput} {id} />
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div onclick={togglePassword} role="button" tabindex="0">
<MdiEyeOffOutline class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}" />
<MdiEyeOutline class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}" />
</div>
@@ -1,8 +1,20 @@
<script lang="ts">
export let min = 0;
export let max = 100;
export let step = 1;
export let value = (max - min) / 2;
import { createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
interface Props {
min?: number;
max?: number;
step?: number;
value?: any;
}
let {
min = 0,
max = 100,
step = 1,
value = $bindable((max - min) / 2)
}: Props = $props();
</script>
<input
@@ -13,8 +25,8 @@
max={max}
step={step}
bind:value
on:input
on:change
oninput={bubble('input')}
onchange={bubble('change')}
/>
<style>
+6 -1
View File
@@ -1,6 +1,11 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="box-border overflow-hidden flex-1">
<slot></slot>
{@render children?.()}
</div>
@@ -1,8 +1,13 @@
<script lang="ts">
import WidgetContainer from './WidgetContainer.svelte';
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application';
import Widget from './Widget.svelte';
export let container: WidgetContainerConfig;
interface Props {
container: WidgetContainerConfig;
}
let { container }: Props = $props();
</script>
<div class="w-full h-full flex flex-col overflow-hidden">
@@ -15,9 +20,10 @@
{#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget>
{#if isWidgetConfig(widget)}
<svelte:component this={WidgetComponents[widget.component]} {...widget.props} />
{@const SvelteComponent = WidgetComponents[widget.component]}
<SvelteComponent {...widget.props} />
{:else if widget.widgets}
<svelte:self container={widget} />
<WidgetContainer container={widget} />
{/if}
</Widget>
{#if index !== container.widgets.length - 1}
@@ -1,7 +1,11 @@
<script lang="ts">
import { Github } from "../icons";
export let github;
interface Props {
github: any;
}
let { github }: Props = $props();
</script>
{#if github.active}
@@ -1,7 +1,8 @@
<script>
import logo from '$lib/assets/logo512.png';
export let appName;
/** @type {{appName: any}} */
let { appName } = $props();
</script>
<a
+105 -98
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import { useFeatureFlags } from '$lib/stores/featureFlags';
@@ -42,102 +44,105 @@
submenu?: menuItem[];
};
$: menuItems = [
{
title: 'Connection',
icon: WiFi,
href: '/connection',
feature: !appEnv.VITE_USE_HOST_NAME
},
{
title: 'Controller',
icon: MdiController,
href: '/controller',
feature: true
},
{
title: 'Peripherals',
icon: Devices,
feature: true,
submenu: [
{
title: 'I2C',
icon: Connection,
href: '/peripherals/i2c',
feature: true
},
{
title: 'Camera',
icon: Camera,
href: '/peripherals/camera',
feature: $features.camera
},
{
title: 'Servo',
icon: MotorOutline,
href: '/peripherals/servo',
feature: true
},
{
title: 'IMU',
icon: Rotate3d,
href: '/peripherals/imu',
feature: $features.imu || $features.mag || $features.bmp
}
]
},
{
title: 'WiFi',
icon: WiFi,
feature: true,
submenu: [
{
title: 'WiFi Station',
icon: Router,
href: '/wifi/sta',
feature: true
},
{
title: 'Access Point',
icon: AP,
href: '/wifi/ap',
feature: true
}
]
},
{
title: 'System',
icon: Settings,
feature: true,
submenu: [
{
title: 'System Status',
icon: Health,
href: '/system/status',
feature: true
},
{
title: 'File System',
icon: Folder,
href: '/system/filesystem',
feature: true
},
{
title: 'System Metrics',
icon: Metrics,
href: '/system/metrics',
feature: $features.analytics
},
{
title: 'Firmware Update',
icon: Update,
href: '/system/update',
feature:
$features.ota || $features.upload_firmware || $features.download_firmware
}
]
}
] as menuItem[];
let menuItems = $state();
run(() => {
menuItems = [
{
title: 'Connection',
icon: WiFi,
href: '/connection',
feature: !appEnv.VITE_USE_HOST_NAME
},
{
title: 'Controller',
icon: MdiController,
href: '/controller',
feature: true
},
{
title: 'Peripherals',
icon: Devices,
feature: true,
submenu: [
{
title: 'I2C',
icon: Connection,
href: '/peripherals/i2c',
feature: true
},
{
title: 'Camera',
icon: Camera,
href: '/peripherals/camera',
feature: $features.camera
},
{
title: 'Servo',
icon: MotorOutline,
href: '/peripherals/servo',
feature: true
},
{
title: 'IMU',
icon: Rotate3d,
href: '/peripherals/imu',
feature: $features.imu || $features.mag || $features.bmp
}
]
},
{
title: 'WiFi',
icon: WiFi,
feature: true,
submenu: [
{
title: 'WiFi Station',
icon: Router,
href: '/wifi/sta',
feature: true
},
{
title: 'Access Point',
icon: AP,
href: '/wifi/ap',
feature: true
}
]
},
{
title: 'System',
icon: Settings,
feature: true,
submenu: [
{
title: 'System Status',
icon: Health,
href: '/system/status',
feature: true
},
{
title: 'File System',
icon: Folder,
href: '/system/filesystem',
feature: true
},
{
title: 'System Metrics',
icon: Metrics,
href: '/system/metrics',
feature: $features.analytics
},
{
title: 'Firmware Update',
icon: Update,
href: '/system/update',
feature:
$features.ota || $features.upload_firmware || $features.download_firmware
}
]
}
] as menuItem[];
});
const dispatch = createEventDispatcher();
@@ -152,7 +157,9 @@
dispatch('menuClicked');
}
$: setActiveMenuItem($page.data.title);
run(() => {
setActiveMenuItem($page.data.title);
});
const updateMenu = (event: any) => {
setActiveMenuItem(event.details);
@@ -164,7 +171,7 @@
<MenuList {menuItems} on:select{updateMenu} class="flex-grow flex-nowrap overflow-y-auto" />
<div class="divider my-0" />
<div class="divider my-0"></div>
<div class="flex items-center justify-between">
<GithubButton {github} />
+11 -16
View File
@@ -1,38 +1,33 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
type menuItem = {
import MenuList from './MenuList.svelte';
type MenuItem = {
title: string;
icon: ConstructorOfATypedSvelteComponent;
href?: string;
feature: boolean;
active?: boolean;
submenu?: menuItem[];
submenu?: MenuItem[];
};
export let menuItems: menuItem[];
export let level = 0;
let { level, menuItems, select, class: klass } = $props();
const selectMenuItem = (title: string) => {
dispatch('select', title);
select(title);
};
</script>
<ul class={$$props.class + ' menu'}>
{#each menuItems as menuItem, i (menuItem.title)}
<ul class={klass + ' menu'}>
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
{#if menuItem.feature}
<li>
{#if menuItem.submenu}
<details open={menuItem.submenu.some(subItem => subItem.active)}>
<summary class="text-lg font-bold">
<svelte:component this={menuItem.icon} class="h-6 w-6" />
<menuItem.icon class="h-6 w-6" />
{menuItem.title}
</summary>
<div class="pl-4">
<svelte:self menuItems={menuItem.submenu} level={level + 1} />
<MenuList menuItems={menuItem.submenu} level={level + 1} />
</div>
</details>
{:else}
@@ -42,9 +37,9 @@
class:bg-base-100={menuItem.active}
class:text-lg={level === 0}
class:text-md={level === 1}
on:click={() => selectMenuItem(menuItem.title)}
onclick={() => selectMenuItem(menuItem.title)}
>
<svelte:component this={menuItem.icon} class="h-6 w-6" />
<menuItem.icon class="h-6 w-6" />
{menuItem.title}
</a>
{/if}
@@ -5,7 +5,11 @@
const features = useFeatureFlags();
export let battery:Battery;
interface Props {
battery: Battery;
}
let { battery }: Props = $props();
const getBatteryIcon = () => {
if (battery.voltage === 0) return BatteryCharging;
@@ -18,9 +22,9 @@
</script>
{#if $features.battery}
{@const SvelteComponent = getBatteryIcon()}
<div class="tooltip tooltip-left z-10" data-tip="{battery.voltage}V {Math.floor(battery.current*10)/10} mA">
<svelte:component
this={getBatteryIcon()}
<SvelteComponent
class="h-7 w-7 -rotate-90 {battery.voltage === 0 || battery.voltage <= 7.6 ? 'animate-pulse' : ''} {battery.voltage <= 7.6 ? 'text-error' : ''}"
/>
</div>
@@ -1,8 +1,10 @@
<script lang="ts">
import { isFullscreen, toggleFullscreen } from '$lib/stores';
import { MdiFullscreenExit, MdiFullscreen } from '../icons';
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen);
</script>
<button on:click={toggleFullscreen}>
<svelte:component this={$isFullscreen ? MdiFullscreenExit : MdiFullscreen} class="h-7 w-7" />
<button onclick={toggleFullscreen}>
<SvelteComponent class="h-7 w-7" />
</button>
@@ -1,8 +1,12 @@
<script lang="ts">
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons";
export let showDBm = false;
export let rssi = 0;
interface Props {
showDBm?: boolean;
rssi?: number;
}
let { showDBm = false, rssi = 0 }: Props = $props();
const getWiFiIcon = () => {
if (rssi === 0) return WifiOff;
@@ -11,6 +15,8 @@
if (rssi >= -85) return WiFi1;
return WiFi0;
};
const SvelteComponent = $derived(getWiFiIcon());
</script>
<div class="indicator">
@@ -21,7 +27,7 @@
</span>
{/if}
<div class="h-7 w-7">
<svelte:component this={getWiFiIcon()} class="absolute inset-0 h-full w-full" />
<SvelteComponent class="absolute inset-0 h-full w-full" />
</div>
</div>
</div>
@@ -27,7 +27,7 @@
{#if $features.sleep}
<div class="flex-none">
<button class="btn btn-square btn-ghost h-9 w-10" on:click={confirmSleep}>
<button class="btn btn-square btn-ghost h-9 w-10" onclick={confirmSleep}>
<Power class="text-error h-9 w-9" />
</button>
</div>
@@ -7,4 +7,4 @@
</script>
<button on:click={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
@@ -5,7 +5,7 @@
<div class="topbar absolute left-0 top-0 w-full z-20 flex justify-between bg-zinc-800">
<div class="flex gap-2 p-2">
<a href="/">
<svelte:component this={Hamburger} class="h-8 w-8"/>
<Hamburger class="h-8 w-8"/>
</a>
</div>
</div>
@@ -14,10 +14,14 @@
const features = useFeatureFlags();
export let update = false;
interface Props {
update?: boolean;
}
let firmwareVersion: string;
let firmwareDownloadLink: string;
let { update = $bindable(false) }: Props = $props();
let firmwareVersion: string = $state();
let firmwareDownloadLink: string = $state();
async function getGithubAPI() {
const headers = {
@@ -100,7 +104,7 @@
<div class="indicator flex-none">
<button
class="btn btn-square btn-ghost h-9 w-9"
on:click={() => confirmGithubUpdate(firmwareDownloadLink)}
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
>
<span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
+7 -6
View File
@@ -4,30 +4,31 @@
import { notifications } from '$lib/components/toasts/notifications';
import { error, info, success, warning } from '../icons';
export let theme = {
/** @type {{theme?: any, icon?: any}} */
let { theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
};
export let icon = {
}, icon = {
error: error,
success: success,
warning: warning,
info: info
};
} } = $props();
</script>
<div class="toast toast-end mr-4 z-20">
{#each $notifications as notification (notification.id)}
{@const SvelteComponent = icon[notification.type]}
<div
animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }}
>
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
<SvelteComponent class="h-6 w-6 flex-shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
@@ -5,12 +5,16 @@
import { cubicOut } from "svelte/easing";
import { slide } from "svelte/transition";
let chartElement: HTMLCanvasElement;
let chartElement: HTMLCanvasElement = $state();
let chart: Chart;
export let label
export let data:number[]
export let title
interface Props {
label: any;
data: number[];
title: any;
}
let { label, data, title }: Props = $props();
Chart.register(...registerables);
@@ -94,6 +98,6 @@
class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={chartElement} />
<canvas bind:this={chartElement}></canvas>
</div>
</div>
+12 -4
View File
@@ -1,12 +1,20 @@
<script lang="ts">
export let options: string[] = [];
export let selectedOption: string = '';
import { createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
interface Props {
options?: string[];
selectedOption?: string;
[key: string]: any
}
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
</script>
<select
bind:value={selectedOption}
on:change
class="select select-bordered select-sm lg:select-md max-w-xs {$$restProps.class || ''}"
onchange={bubble('change')}
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
>
{#each options as option}
<option value={option}>{option}</option>
+319 -319
View File
@@ -1,26 +1,26 @@
import {
Mesh,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer,
AmbientLight,
DirectionalLight,
PCFSoftShadowMap,
GridHelper,
ArrowHelper,
Vector3,
FogExp2,
CanvasTexture,
type ColorRepresentation,
type WebGLRendererParameters,
MeshPhongMaterial,
EquirectangularReflectionMapping,
ACESFilmicToneMapping,
MathUtils,
Group,
MeshBasicMaterial,
RepeatWrapping
Mesh,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer,
AmbientLight,
DirectionalLight,
PCFSoftShadowMap,
type GridHelper,
ArrowHelper,
Vector3,
FogExp2,
CanvasTexture,
type ColorRepresentation,
type WebGLRendererParameters,
MeshPhongMaterial,
EquirectangularReflectionMapping,
ACESFilmicToneMapping,
MathUtils,
Group,
MeshBasicMaterial,
RepeatWrapping
} from 'three';
import { Sky } from 'three/addons/objects/Sky.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
@@ -33,347 +33,347 @@ import { sunCalculator } from './utilities/position-utilities';
export const addScene = () => new Scene();
interface position {
x?: number;
y?: number;
z?: number;
x?: number;
y?: number;
z?: number;
}
interface light {
color?: ColorRepresentation;
intensity?: number;
color?: ColorRepresentation;
intensity?: number;
}
interface arrowOptions {
origin: position;
direction: position;
length?: number;
color?: ColorRepresentation;
origin: position;
direction: position;
length?: number;
color?: ColorRepresentation;
}
type directionalLight = position & light;
export default class SceneBuilder {
public scene: Scene;
public camera!: PerspectiveCamera;
public ground!: Mesh;
public renderer!: WebGLRenderer;
public orbit: OrbitControls;
public callback: Function | undefined;
public gridHelper!: GridHelper;
public model!: URDFRobot;
public liveStreamTexture!: CanvasTexture;
private fog!: FogExp2;
private isLoaded: boolean = false;
public isDragging: boolean = false;
highlightMaterial: any;
sky!: Sky;
transformControl: TransformControls;
public modelGroup!: Group;
public scene: Scene;
public camera!: PerspectiveCamera;
public ground!: Mesh;
public renderer!: WebGLRenderer;
public orbit: OrbitControls;
public callback: Function | undefined;
public gridHelper!: GridHelper;
public model!: URDFRobot;
public liveStreamTexture!: CanvasTexture;
private fog!: FogExp2;
private isLoaded: boolean = false;
public isDragging: boolean = false;
highlightMaterial: any;
sky!: Sky;
transformControl: TransformControls;
public modelGroup!: Group;
constructor() {
this.scene = new Scene();
if (this.scene.environment?.mapping) {
this.scene.environment.mapping = EquirectangularReflectionMapping;
}
return this;
}
constructor() {
this.scene = new Scene();
if (this.scene.environment?.mapping) {
this.scene.environment.mapping = EquirectangularReflectionMapping;
}
return this;
}
public addRenderer = (parameters?: WebGLRendererParameters) => {
this.renderer = new WebGLRenderer(parameters);
this.renderer.outputColorSpace = 'srgb';
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = PCFSoftShadowMap;
this.renderer.toneMapping = ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 0.85;
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement);
return this;
};
public addRenderer = (parameters?: WebGLRendererParameters) => {
this.renderer = new WebGLRenderer(parameters);
this.renderer.outputColorSpace = 'srgb';
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = PCFSoftShadowMap;
this.renderer.toneMapping = ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 0.85;
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement);
return this;
};
public addSky = () => {
this.sky = new Sky();
this.sky.scale.setScalar(450000);
this.scene.add(this.sky);
const effectController = {
turbidity: 10,
rayleigh: 3,
mieCoefficient: 0.005,
mieDirectionalG: 0.7,
elevation: sunCalculator.calculateSunElevation(),
azimuth: 200,
exposure: this.renderer.toneMappingExposure
};
const uniforms = this.sky.material.uniforms;
uniforms['turbidity'].value = effectController.turbidity;
uniforms['rayleigh'].value = effectController.rayleigh;
uniforms['mieCoefficient'].value = effectController.mieCoefficient;
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG;
this.renderer.toneMappingExposure = 0.5;
const phi = MathUtils.degToRad(90 - effectController.elevation);
const theta = MathUtils.degToRad(effectController.azimuth);
const sun = new Vector3();
public addSky = () => {
this.sky = new Sky();
this.sky.scale.setScalar(450000);
this.scene.add(this.sky);
const effectController = {
turbidity: 10,
rayleigh: 3,
mieCoefficient: 0.005,
mieDirectionalG: 0.7,
elevation: sunCalculator.calculateSunElevation(),
azimuth: 200,
exposure: this.renderer.toneMappingExposure
};
const uniforms = this.sky.material.uniforms;
uniforms['turbidity'].value = effectController.turbidity;
uniforms['rayleigh'].value = effectController.rayleigh;
uniforms['mieCoefficient'].value = effectController.mieCoefficient;
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG;
this.renderer.toneMappingExposure = 0.5;
const phi = MathUtils.degToRad(90 - effectController.elevation);
const theta = MathUtils.degToRad(effectController.azimuth);
const sun = new Vector3();
sun.setFromSphericalCoords(1, phi, theta);
uniforms['sunPosition'].value.copy(sun);
return this;
};
sun.setFromSphericalCoords(1, phi, theta);
uniforms['sunPosition'].value.copy(sun);
return this;
};
public addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera();
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0);
this.scene.add(this.camera);
return this;
};
public addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera();
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0);
this.scene.add(this.camera);
return this;
};
public addGroundPlane = (options?: position) => {
const checkerboardTexture = this.createCheckerboardTexture(1024, 2);
checkerboardTexture.wrapS = RepeatWrapping;
checkerboardTexture.wrapT = RepeatWrapping;
checkerboardTexture.repeat.set(100, 100);
const checkerboardMat = new MeshBasicMaterial({
map: checkerboardTexture,
opacity: 0.1,
transparent: true
});
public addGroundPlane = (options?: position) => {
const checkerboardTexture = this.createCheckerboardTexture(1024, 2);
checkerboardTexture.wrapS = RepeatWrapping;
checkerboardTexture.wrapT = RepeatWrapping;
checkerboardTexture.repeat.set(100, 100);
const checkerboardMat = new MeshBasicMaterial({
map: checkerboardTexture,
opacity: 0.1,
transparent: true
});
const plane = new PlaneGeometry(400, 400);
const plane = new PlaneGeometry(400, 400);
this.ground = new Mesh(plane, checkerboardMat);
this.ground.rotation.x = -Math.PI / 2;
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0);
this.ground.receiveShadow = true;
this.scene.add(this.ground);
this.ground = new Mesh(plane, checkerboardMat);
this.ground.rotation.x = -Math.PI / 2;
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0);
this.ground.receiveShadow = true;
this.scene.add(this.ground);
const mirror = new Reflector(plane, {
clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0x00bfff
});
mirror.rotateX(-Math.PI / 2);
this.scene.add(mirror);
const mirror = new Reflector(plane, {
clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0x00bfff
});
mirror.rotateX(-Math.PI / 2);
this.scene.add(mirror);
return this;
};
return this;
};
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
this.orbit.minDistance = minDistance;
this.orbit.maxDistance = maxDistance;
this.orbit.autoRotate = autoRotate;
this.orbit.update();
return this;
};
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
this.orbit.minDistance = minDistance;
this.orbit.maxDistance = maxDistance;
this.orbit.autoRotate = autoRotate;
this.orbit.update();
return this;
};
public addAmbientLight = (options: light) => {
const ambientLight = new AmbientLight(options.color, options.intensity);
this.scene.add(ambientLight);
return this;
};
public addAmbientLight = (options: light) => {
const ambientLight = new AmbientLight(options.color, options.intensity);
this.scene.add(ambientLight);
return this;
};
public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity);
directionalLight.castShadow = true;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.mapSize.set(4096, 4096);
public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity);
directionalLight.castShadow = true;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.mapSize.set(4096, 4096);
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.scene.add(directionalLight);
return this;
};
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.scene.add(directionalLight);
return this;
};
private createCheckerboardTexture = (size: number, squares: number) => {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const context = canvas.getContext('2d');
private createCheckerboardTexture = (size: number, squares: number) => {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const context = canvas.getContext('2d');
const squareSize = size / squares;
const squareSize = size / squares;
for (let y = 0; y < squares; y++) {
for (let x = 0; x < squares; x++) {
context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000';
context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
}
}
for (let y = 0; y < squares; y++) {
for (let x = 0; x < squares; x++) {
context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000';
context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
}
}
const texture = new CanvasTexture(canvas);
texture.wrapS = texture.wrapT = RepeatWrapping;
texture.anisotropy = 16;
return texture;
};
const texture = new CanvasTexture(canvas);
texture.wrapS = texture.wrapT = RepeatWrapping;
texture.anisotropy = 16;
return texture;
};
public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.scene.fog = new FogExp2(color, density);
return this;
};
public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.scene.fog = new FogExp2(color, density);
return this;
};
public fillParent = () => {
const parentElement = this.renderer.domElement.parentElement;
if (parentElement) {
const width = parentElement.clientWidth;
const height = parentElement.clientHeight;
this.handleResize(width, height);
}
return this;
};
public fillParent = () => {
const parentElement = this.renderer.domElement.parentElement;
if (parentElement) {
const width = parentElement.clientWidth;
const height = parentElement.clientHeight;
this.handleResize(width, height);
}
return this;
};
public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
return this;
};
public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
return this;
};
public addRenderCb = (callback: Function) => {
this.callback = callback;
return this;
};
public addRenderCb = (callback: Function) => {
this.callback = callback;
return this;
};
public startRenderLoop = () => {
this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera);
this.orbit.update();
this.handleRobotShadow();
if (this.callback) this.callback();
if (!this.liveStreamTexture) return;
});
return this;
};
public startRenderLoop = () => {
this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera);
this.orbit.update();
this.handleRobotShadow();
if (this.callback) this.callback();
if (!this.liveStreamTexture) return;
});
return this;
};
public addArrowHelper = (options?: arrowOptions) => {
const dir = new Vector3(
options?.direction.x ?? 0,
options?.direction.y ?? 0,
options?.direction.z ?? 0
);
const origin = new Vector3(
options?.origin.x ?? 0,
options?.origin.y ?? 0,
options?.origin.z ?? 0
);
const arrowHelper = new ArrowHelper(
dir,
origin,
options?.length ?? 1.5,
options?.color ?? 0xff0000
);
this.scene.add(arrowHelper);
return this;
};
public addArrowHelper = (options?: arrowOptions) => {
const dir = new Vector3(
options?.direction.x ?? 0,
options?.direction.y ?? 0,
options?.direction.z ?? 0
);
const origin = new Vector3(
options?.origin.x ?? 0,
options?.origin.y ?? 0,
options?.origin.z ?? 0
);
const arrowHelper = new ArrowHelper(
dir,
origin,
options?.length ?? 1.5,
options?.color ?? 0xff0000
);
this.scene.add(arrowHelper);
return this;
};
private setJointValue(jointName: string, angle: number) {
if (!this.model) return;
if (!this.model.joints[jointName]) return;
this.model.joints[jointName].setJointValue(angle);
}
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';
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed';
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
const traverse = (c: any) => {
if (c.type === 'Mesh') {
if (revert) {
c.material = c.__origMaterial;
delete c.__origMaterial;
} else {
c.__origMaterial = c.material;
c.material = material;
}
}
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
const traverse = (c: any) => {
if (c.type === 'Mesh') {
if (revert) {
c.material = c.__origMaterial;
delete c.__origMaterial;
} else {
c.__origMaterial = c.material;
c.material = material;
}
}
if (c === m || !this.isJoint(c)) {
for (let i = 0; i < c.children.length; i++) {
const child = c.children[i];
if (!child.isURDFCollider) {
traverse(c.children[i]);
}
}
}
};
traverse(m);
};
if (c === m || !this.isJoint(c)) {
for (let i = 0; i < c.children.length; i++) {
const child = c.children[i];
if (!child.isURDFCollider) {
traverse(c.children[i]);
}
}
}
};
traverse(m);
};
public addTransformControls = (model: any) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement);
this.transformControl.addEventListener('dragging-changed', (event: any) => {
this.orbit.enabled = !event.value;
this.isDragging = !event.value;
});
this.transformControl.attach(model);
this.scene.add(this.transformControl);
this.transformControl.setMode('rotate');
return this;
};
public addTransformControls = (model: any) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement);
this.transformControl.addEventListener('dragging-changed', (event: any) => {
this.orbit.enabled = !event.value;
this.isDragging = !event.value;
});
this.transformControl.attach(model);
this.scene.add(this.transformControl);
this.transformControl.setMode('rotate');
return this;
};
public addModel = (model: any) => {
this.modelGroup = new Group();
this.modelGroup.add(model);
this.model = model;
this.scene.add(this.modelGroup);
return this;
};
public addModel = (model: any) => {
this.modelGroup = new Group();
this.modelGroup.add(model);
this.model = model;
this.scene.add(this.modelGroup);
return this;
};
public addDragControl = (updateAngle: any) => {
const highlightColor = '#FFFFFF';
const highlightMaterial = new MeshPhongMaterial({
shininess: 10,
color: highlightColor,
emissive: highlightColor,
emissiveIntensity: 0.9
});
public addDragControl = (updateAngle: any) => {
const highlightColor = '#FFFFFF';
const highlightMaterial = new MeshPhongMaterial({
shininess: 10,
color: highlightColor,
emissive: highlightColor,
emissiveIntensity: 0.9
});
const dragControls = new PointerURDFDragControls(
this.scene,
this.camera,
this.renderer.domElement
);
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
this.setJointValue(joint.name, angle);
updateAngle(joint.name, angle);
};
dragControls.onDragStart = () => {
this.orbit.enabled = false;
this.isDragging = true;
};
dragControls.onDragEnd = () => {
this.orbit.enabled = true;
this.isDragging = false;
};
dragControls.onHover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, false, highlightMaterial);
dragControls.onUnhover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, true, highlightMaterial);
const dragControls = new PointerURDFDragControls(
this.scene,
this.camera,
this.renderer.domElement
);
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
this.setJointValue(joint.name, angle);
updateAngle(joint.name, angle);
};
dragControls.onDragStart = () => {
this.orbit.enabled = false;
this.isDragging = true;
};
dragControls.onDragEnd = () => {
this.orbit.enabled = true;
this.isDragging = false;
};
dragControls.onHover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, false, highlightMaterial);
dragControls.onUnhover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, true, highlightMaterial);
this.renderer.domElement.addEventListener(
'touchstart',
(data) => dragControls._mouseDown(data.touches[0]),
{ passive: true }
);
this.renderer.domElement.addEventListener(
'touchmove',
(data) => dragControls._mouseMove(data.touches[0]),
{ passive: true }
);
this.renderer.domElement.addEventListener(
'touchend',
(data) => dragControls._mouseUp(data.touches[0]),
{ passive: true }
);
return this;
};
this.renderer.domElement.addEventListener(
'touchstart',
data => dragControls._mouseDown(data.touches[0]),
{ passive: true }
);
this.renderer.domElement.addEventListener(
'touchmove',
data => dragControls._mouseMove(data.touches[0]),
{ passive: true }
);
this.renderer.domElement.addEventListener(
'touchend',
data => dragControls._mouseUp(data.touches[0]),
{ passive: true }
);
return this;
};
public toggleFog = () => {
this.scene.fog = this.scene.fog ? null : this.fog;
};
public toggleFog = () => {
this.scene.fog = this.scene.fog ? null : this.fog;
};
private handleRobotShadow = () => {
if (this.isLoaded) return;
const intervalId = setInterval(() => this.model?.traverse((c) => (c.castShadow = true)), 10);
setTimeout(() => clearInterval(intervalId), 1000);
this.isLoaded = true;
};
private handleRobotShadow = () => {
if (this.isLoaded) return;
const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10);
setTimeout(() => clearInterval(intervalId), 1000);
this.isLoaded = true;
};
}