@viamrobotics/motion-tools 1.25.4 → 1.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
3
 
4
- import { T } from '@threlte/core'
4
+ import { T, useThrelte } from '@threlte/core'
5
5
  import { Environment, Grid, interactivity, PerfMonitor, PortalTarget } from '@threlte/extras'
6
6
  import { useXR } from '@threlte/xr'
7
7
  import { ShaderMaterial, Vector3 } from 'three'
@@ -29,10 +29,14 @@
29
29
 
30
30
  let { children }: Props = $props()
31
31
 
32
+ const threlte = useThrelte()
32
33
  const settings = useSettings()
33
34
  const focusedObject3d = useFocusedObject3d()
34
35
  const origin = useOrigin()
35
36
 
37
+ // @ts-expect-error This is for debugging
38
+ globalThis.__threlte__ = threlte
39
+
36
40
  const { raycaster, enabled } = interactivity({
37
41
  filter: (intersections) => {
38
42
  const match = intersections.find((intersection) => {
@@ -477,13 +477,24 @@
477
477
  <div>
478
478
  <strong class="font-semibold">parent frame</strong>
479
479
  {#if showEditFrameOptions}
480
- <div aria-label="mutable parent frame">
481
- <List
482
- options={configFrames.getParentFrameOptions(name.current ?? '') ?? []}
483
- value={parent.current ?? 'world'}
484
- on:change={handleParentChange}
485
- />
486
- </div>
480
+ <!--
481
+ Remount on entity change. svelte-tweakpane-ui's List runs
482
+ `listBlade.value = value` on the still-mounted blade before its
483
+ `options` prop has propagated, so the new entity's parent name
484
+ (absent from the previous entity's option set) hits Tweakpane's
485
+ ListConstraint, snaps to the first option, and fires a change
486
+ event that handleParentChange interprets as a user pick — silently
487
+ reparenting the clicked frame.
488
+ -->
489
+ {#key entity}
490
+ <div aria-label="mutable parent frame">
491
+ <List
492
+ options={configFrames.getParentFrameOptions(name.current ?? '') ?? []}
493
+ value={parent.current ?? 'world'}
494
+ on:change={handleParentChange}
495
+ />
496
+ </div>
497
+ {/key}
487
498
  {:else}
488
499
  <div class="mt-0.5 flex gap-3">
489
500
  {@render ImmutableField({
@@ -89,11 +89,19 @@
89
89
  </div>
90
90
  </div>
91
91
 
92
+ <!--
93
+ Skip rendering the body subtree while collapsed. zag-js controls
94
+ visibility via attributes (the panel chrome stays mounted), but the
95
+ children don't need to react to upstream state when the user can't see them.
96
+ Children mount fresh on open.
97
+ -->
92
98
  <div
93
99
  {...api.getBodyProps()}
94
100
  class="relative h-[calc(100%-33px)]"
95
101
  >
96
- {@render children()}
102
+ {#if isOpen}
103
+ {@render children()}
104
+ {/if}
97
105
  </div>
98
106
 
99
107
  {#if resizable}
@@ -10,6 +10,12 @@ export declare const parentTraits: (name: string | undefined) => ConfigurableTra
10
10
  * Set or clear an entity's parent. Strips any existing `ChildOf` or `Orphan`,
11
11
  * then writes `Orphan(name)` (the resolver converts it to `ChildOf` on the
12
12
  * next reactive flush). Pass `undefined` or `'world'` to detach to root.
13
+ *
14
+ * Short-circuits when the effective parent name (via resolved `ChildOf` or
15
+ * pending `Orphan`) already matches `name`. Network-backed reconcilers call
16
+ * this every refetch tick on stable entities; the demote-then-re-resolve
17
+ * dance otherwise flips `useParentName` to `undefined` and back, remounting
18
+ * every `<Portal id={parent.current}>` subtree per tick.
13
19
  */
14
20
  export declare const setParent: (entity: Entity, name: string | undefined) => void;
15
21
  /** The parent entity, or `undefined` at the world root or while orphaned. */
@@ -16,15 +16,25 @@ export const parentTraits = (name) => {
16
16
  * Set or clear an entity's parent. Strips any existing `ChildOf` or `Orphan`,
17
17
  * then writes `Orphan(name)` (the resolver converts it to `ChildOf` on the
18
18
  * next reactive flush). Pass `undefined` or `'world'` to detach to root.
19
+ *
20
+ * Short-circuits when the effective parent name (via resolved `ChildOf` or
21
+ * pending `Orphan`) already matches `name`. Network-backed reconcilers call
22
+ * this every refetch tick on stable entities; the demote-then-re-resolve
23
+ * dance otherwise flips `useParentName` to `undefined` and back, remounting
24
+ * every `<Portal id={parent.current}>` subtree per tick.
19
25
  */
20
26
  export const setParent = (entity, name) => {
27
+ const desired = !name || name === 'world' ? undefined : name;
21
28
  const target = entity.targetFor(ChildOf);
29
+ const current = (target?.isAlive() ? target.get(Name) : undefined) ?? entity.get(Orphan);
30
+ if (current === desired)
31
+ return;
22
32
  if (target)
23
33
  entity.remove(ChildOf(target));
24
34
  entity.remove(Orphan);
25
- if (!name || name === 'world')
35
+ if (desired === undefined)
26
36
  return;
27
- entity.add(Orphan(name));
37
+ entity.add(Orphan(desired));
28
38
  };
29
39
  /** The parent entity, or `undefined` at the world root or while orphaned. */
30
40
  export const getParentEntity = (entity) => entity.targetFor(ChildOf);
@@ -1,8 +1,16 @@
1
1
  import { createWorld } from 'koota';
2
2
  import { getContext, setContext } from 'svelte';
3
+ import * as relations from './relations';
4
+ import * as traits from './traits';
3
5
  export const WORLD_CONTEXT_KEY = Symbol('koota-context');
4
6
  export function provideWorld() {
5
7
  const world = createWorld();
8
+ // @ts-expect-error This is for debugging.
9
+ globalThis.__koota__ = {
10
+ world,
11
+ traits,
12
+ relations,
13
+ };
6
14
  setContext(WORLD_CONTEXT_KEY, world);
7
15
  }
8
16
  export function useWorld() {
@@ -1,17 +1,31 @@
1
1
  import { getContext, setContext, untrack } from 'svelte';
2
2
  import { MathUtils } from 'three';
3
3
  const key = Symbol('logs-context');
4
+ const MAX_LOGS = 200;
4
5
  export const provideLogs = () => {
5
- const logs = $state([]);
6
- const warnings = $state([]);
7
- const errors = $state([]);
6
+ // Plain insertion-ordered Map keyed by `${level}|${timestamp}|${message}`
7
+ // drives storage; a single `$state` version counter drives reactivity.
8
+ // Hot path is `Map.set` (or in-place `count++`) plus `version++` — no
9
+ // array allocation per add. The display arrays are materialized lazily
10
+ // in `$derived.by`, so a closed logs panel costs nothing.
11
+ const entries = new Map();
12
+ let version = $state(0);
8
13
  const intl = new Intl.DateTimeFormat('en-US', {
9
14
  dateStyle: 'short',
10
15
  timeStyle: 'short',
11
16
  });
17
+ const dedupKey = (timestamp, level, message) => `${level}\0${timestamp}\0${message}`;
18
+ const all = $derived.by(() => {
19
+ void version;
20
+ const out = [...entries.values()];
21
+ out.reverse();
22
+ return out;
23
+ });
24
+ const errors = $derived(all.filter((l) => l.level === 'error'));
25
+ const warnings = $derived(all.filter((l) => l.level === 'warn'));
12
26
  setContext(key, {
13
27
  get current() {
14
- return logs;
28
+ return all;
15
29
  },
16
30
  get errors() {
17
31
  return errors;
@@ -22,35 +36,26 @@ export const provideLogs = () => {
22
36
  add(message, level = 'info') {
23
37
  untrack(() => {
24
38
  const timestamp = intl.format(Date.now());
25
- const match = logs.find((log) => log.message === message && log.level === level && log.timestamp === timestamp);
39
+ const k = dedupKey(timestamp, level, message);
40
+ const match = entries.get(k);
26
41
  if (match) {
27
42
  match.count += 1;
28
43
  }
29
44
  else {
30
- const log = {
31
- timestamp,
45
+ entries.set(k, {
46
+ uuid: MathUtils.generateUUID(),
32
47
  message,
33
48
  count: 1,
34
49
  level,
35
- uuid: MathUtils.generateUUID(),
36
- };
37
- logs.unshift(log);
38
- if (level === 'error') {
39
- errors.unshift(log);
40
- }
41
- else if (level === 'warn') {
42
- warnings.unshift(log);
43
- }
44
- }
45
- if (logs.length > 200) {
46
- const log = logs.pop();
47
- if (log?.level === 'error') {
48
- errors.splice(errors.indexOf(log), 1);
49
- }
50
- else if (log?.level === 'warn') {
51
- warnings.splice(warnings.indexOf(log), 1);
50
+ timestamp,
51
+ });
52
+ if (entries.size > MAX_LOGS) {
53
+ const oldestKey = entries.keys().next().value;
54
+ if (oldestKey !== undefined)
55
+ entries.delete(oldestKey);
52
56
  }
53
57
  }
58
+ version++;
54
59
  });
55
60
  },
56
61
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viamrobotics/motion-tools",
3
- "version": "1.25.4",
3
+ "version": "1.25.6",
4
4
  "description": "Motion visualization with Viam",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",