Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Verdicts: OK · FIX(ed this batch) · DOC(umented quirk).
| customnode | (meta) | effect (drives a Selector) | per-def param | FIX (4.5): def params get INPUT sockets (they were unwirable — the runtime already resolved them); def edits prune dangling edges deterministically. |
| maprange | Logic | number | a | NEW (4.6): remap [inMin..inMax] → [outMin..outMax], optional clamp — the glue between free-range sources and bounded params. |
| select | Logic | number | index a b | NEW (4.6): outputs a when index < 0.5 else b — pairs with switcher-as-number / compare. |
| collider | Physics | effect | source scale | NEW (CL-C): overrides the Inspector collider pick at sim start AND live (mid-sim rebuild — no restart). `shape` box/sphere/capsule/cylinder/hull/custom/object; `object` hulls the geometry of the Object Selector wired into `source`; `scale` multiplies the shape; `sensor` makes every piece a trigger volume. Consumed by physics.js collectParams (not per-frame eval), like mass. |
| onenter / onexit | Triggers | event | — | NEW (CL-C): pulse when a SENSOR pair starts/stops overlapping (initiator-detected in the substep drain, both directions of the pair, per-frame dedupe; sensors bypass the impact downward-velocity filter). Same replicated nodetrigger stamps + BFS/implicit-owner targeting as onimpact. Recipe: sensor box + On Enter -> Particles = checkpoint confetti; On Enter -> Counter = lap counter. |
| velocity | Scene | number | target | NEW (CL-C): live speed (m/s) of the wired object (or the graph owner unwired in an object graph). LOCAL feed — exact-ish on the sim initiator (per-step write-back deltas), an ~10Hz move-delta APPROXIMATION on other peers (broadcast-gated; documented on the card); 0 at rest / feed quiet >400ms. Never replicated: peers derive their own estimate. |

## Correctness fixes shipped in 4.1
- **Edge-id collision (divergence bug)**: ids were `e-<src>-<tgt>` WITHOUT handles, so
Expand Down
8 changes: 6 additions & 2 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,13 @@
import('./lib/version'),
import('./lib/whatsNew'),
import('./lib/confirmDialog'),
import('./lib/scenePhysics'),
import('./lib/colliderSpec'),
import('./lib/colliderHelpers'),
import('./lib/colliderEdit'),
import('./lib/trackpadNav')
]).then(([sceneStore, appStore, flowStore, meshEdit, vrControls, autosave, voiceChat, annotationsHandler, flowRuntime, history, materialsHandler, objectActions, commandsHandler, moduleSDK, drawModeLib, pathCapture, lockControl, prefabsLib, physics, jointsLib, possessLib, handModelsLib, terrainSculptLib, userModulesLib, environmentLib, sceneMusicLib, animatedImports, fileHandler, fileWindowsLib, sceneBounds, cameraClip, ping, sessionsLib, geometryEdit, lightParams, shadowDefaultsLib, paletteLib, viewModeLib, inputRuntimeLib, shortcutsLib, themesLib, vrRadialMenu, vrPaletteLib, vrWindowPosesLib, vrKeyboardLib, faceEditLib, avatarModelLib, explorerLib, bottomDock, explorerDrop, assetShare, soundRuntime, dungeonPlay, sceneAssetsLib, THREE, GLTFExporterModule, snappingLib, flowSocketsLib, networkQualityLib, packsLib, customNodesLib, nodesHandlerLib, nodeCatalogLib, objectMenuLib, animationPreviewLib, aiProvidersLib, aiToolsLib, aiAssistantLib, meshProvidersLib, meshJobsLib, flowGraphsLib, objectFlowLib, peerServerLib, cloudHooksLib, cloudPluginLib, connectionStateLib, peerApprovalLib, particleRuntimeLib, particleActionsLib, particlePresetsLib, versionLib, whatsNewLib, confirmDialogLib, trackpadNavLib]) => {
window.__stores = { ...sceneStore, ...appStore, ...flowStore, meshEdit, vrControls, autosave, voiceChat, annotationsHandler, flowRuntime, history, materialsHandler, objectActions, commandsHandler, moduleSDK, drawMode: drawModeLib, pathCapture, lockControl, prefabs: prefabsLib, physics, joints: jointsLib, possess: possessLib, handModels: handModelsLib, terrainSculpt: terrainSculptLib, userModules: userModulesLib, environment: environmentLib, sceneMusic: sceneMusicLib, animatedImports, fileHandler, fileWindows: fileWindowsLib, sceneBounds, cameraClip, ping, sessions: sessionsLib, geometryEdit, lightParams, shadowDefaults: shadowDefaultsLib, palette: paletteLib, viewModeCtl: viewModeLib, inputRuntime: inputRuntimeLib, shortcutsRegistry: shortcutsLib, themes: themesLib, vrRadialMenu, vrPalette: vrPaletteLib, vrWindowPoses: vrWindowPosesLib, vrKeyboard: vrKeyboardLib, faceEdit: faceEditLib, avatarModel: avatarModelLib, explorer: explorerLib, bottomDock, explorerDrop, assetShare, soundRuntime, dungeonPlay, sceneAssets: sceneAssetsLib, THREE, GLTFExporterModule, snapping: snappingLib, flowSockets: flowSocketsLib, networkQuality: networkQualityLib, packs: packsLib, customNodes: customNodesLib, nodesHandler: nodesHandlerLib, nodeCatalog: nodeCatalogLib, objectMenu: objectMenuLib, animationPreview: animationPreviewLib, aiProviders: aiProvidersLib, aiTools: aiToolsLib, aiAssistant: aiAssistantLib, meshProviders: meshProvidersLib, meshJobs: meshJobsLib, flowGraphsCtl: flowGraphsLib, objectFlow: objectFlowLib, peerServer: peerServerLib, cloudHooks: cloudHooksLib, cloudPlugin: cloudPluginLib, connectionState: connectionStateLib, peerApproval: peerApprovalLib, particleRuntime: particleRuntimeLib, particleActions: particleActionsLib, particlePresets: particlePresetsLib, version: versionLib, whatsNew: whatsNewLib, confirmDialog: confirmDialogLib, trackpadNav: trackpadNavLib }
]).then(([sceneStore, appStore, flowStore, meshEdit, vrControls, autosave, voiceChat, annotationsHandler, flowRuntime, history, materialsHandler, objectActions, commandsHandler, moduleSDK, drawModeLib, pathCapture, lockControl, prefabsLib, physics, jointsLib, possessLib, handModelsLib, terrainSculptLib, userModulesLib, environmentLib, sceneMusicLib, animatedImports, fileHandler, fileWindowsLib, sceneBounds, cameraClip, ping, sessionsLib, geometryEdit, lightParams, shadowDefaultsLib, paletteLib, viewModeLib, inputRuntimeLib, shortcutsLib, themesLib, vrRadialMenu, vrPaletteLib, vrWindowPosesLib, vrKeyboardLib, faceEditLib, avatarModelLib, explorerLib, bottomDock, explorerDrop, assetShare, soundRuntime, dungeonPlay, sceneAssetsLib, THREE, GLTFExporterModule, snappingLib, flowSocketsLib, networkQualityLib, packsLib, customNodesLib, nodesHandlerLib, nodeCatalogLib, objectMenuLib, animationPreviewLib, aiProvidersLib, aiToolsLib, aiAssistantLib, meshProvidersLib, meshJobsLib, flowGraphsLib, objectFlowLib, peerServerLib, cloudHooksLib, cloudPluginLib, connectionStateLib, peerApprovalLib, particleRuntimeLib, particleActionsLib, particlePresetsLib, versionLib, whatsNewLib, confirmDialogLib, scenePhysicsLib, colliderSpecLib, colliderHelpersLib, colliderEditLib, trackpadNavLib]) => {
window.__stores = { ...sceneStore, ...appStore, ...flowStore, meshEdit, vrControls, autosave, voiceChat, annotationsHandler, flowRuntime, history, materialsHandler, objectActions, commandsHandler, moduleSDK, drawMode: drawModeLib, pathCapture, lockControl, prefabs: prefabsLib, physics, joints: jointsLib, possess: possessLib, handModels: handModelsLib, terrainSculpt: terrainSculptLib, userModules: userModulesLib, environment: environmentLib, sceneMusic: sceneMusicLib, animatedImports, fileHandler, fileWindows: fileWindowsLib, sceneBounds, cameraClip, ping, sessions: sessionsLib, geometryEdit, lightParams, shadowDefaults: shadowDefaultsLib, palette: paletteLib, viewModeCtl: viewModeLib, inputRuntime: inputRuntimeLib, shortcutsRegistry: shortcutsLib, themes: themesLib, vrRadialMenu, vrPalette: vrPaletteLib, vrWindowPoses: vrWindowPosesLib, vrKeyboard: vrKeyboardLib, faceEdit: faceEditLib, avatarModel: avatarModelLib, explorer: explorerLib, bottomDock, explorerDrop, assetShare, soundRuntime, dungeonPlay, sceneAssets: sceneAssetsLib, THREE, GLTFExporterModule, snapping: snappingLib, flowSockets: flowSocketsLib, networkQuality: networkQualityLib, packs: packsLib, customNodes: customNodesLib, nodesHandler: nodesHandlerLib, nodeCatalog: nodeCatalogLib, objectMenu: objectMenuLib, animationPreview: animationPreviewLib, aiProviders: aiProvidersLib, aiTools: aiToolsLib, aiAssistant: aiAssistantLib, meshProviders: meshProvidersLib, meshJobs: meshJobsLib, flowGraphsCtl: flowGraphsLib, objectFlow: objectFlowLib, peerServer: peerServerLib, cloudHooks: cloudHooksLib, cloudPlugin: cloudPluginLib, connectionState: connectionStateLib, peerApproval: peerApprovalLib, particleRuntime: particleRuntimeLib, particleActions: particleActionsLib, particlePresets: particlePresetsLib, version: versionLib, whatsNew: whatsNewLib, confirmDialog: confirmDialogLib, scenePhysics: scenePhysicsLib, colliderSpec: colliderSpecLib, colliderHelpers: colliderHelpersLib, colliderEdit: colliderEditLib, trackpadNav: trackpadNavLib }
})
}
})
Expand Down
23 changes: 17 additions & 6 deletions src/components/Scene.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import { capturePathClick } from '$lib/pathCapture';
import { surfaceSnap, dropToSurface } from '$lib/snapping';
import { editingObject, exitEditMode, raycastHandles, onProxyMoved, onProxyDragChanged, tickMeshEdit } from '$lib/meshEdit';
import { faceEditObject, commitArmedFaceOp, exitFaceEdit, highlightFaceByTriangle, attachFaceGizmo, onFaceGizmoMoved, onFaceGizmoDragChanged, autoApplyFaceOp, faceEditMulti, toggleFaceSelection } from '$lib/faceEdit';
import { faceEditObject, faceEditOp, commitArmedFaceOp, exitFaceEdit, highlightFaceByTriangle, attachFaceGizmo, detachFaceGizmo, onFaceGizmoMoved, onFaceGizmoDragChanged, autoApplyFaceOp, faceEditMulti, toggleFaceSelection, lookupEditable } from '$lib/faceEdit';
import { fireObjectClick } from '$lib/flowRuntime';
import { initVRControls, updateVRControls, raycastMenu, raycastPanel, raycastPalette, raycastProps, raycastPrefabs, raycastKeyboard, raycastChat, raycastEdit, raycastSnap, raycastSettings, raycastApprove, placePrefabGhost, vrFaceTrigger, vrVertexTrigger, vrVertexGrabStart, vrVertexGrabEnd, beginStretchSliderDrag, endStretchSliderDrag, executeVRMenuAction, resetWorldRig, onInputSourcesChange, worldToContentPose, boxSelectStart, boxSelectEnd, boxSelectActive, applyVRFrameRate, shouldSendHands, onHandPinchStart, onHandPinchEnd, pinchMenuToggledAt, firePingIfArmed } from '$lib/vrControls';
import { vrKeyboardTarget } from '$lib/vrKeyboard';
Expand All @@ -30,6 +30,7 @@
import { setParticleRoot } from '$lib/particleRuntime';
import { sendPing } from '$lib/ping';
import { startLightHelpers, updateLightHelpers, lightProxiesGroup } from '$lib/lightHelpers';
import { startColliderHelpers, updateColliderHelpers } from '$lib/colliderHelpers';
import { startEditorNavigation, updateEditorNavigation } from '$lib/editorNavigation';
import { vrMenuOpen } from '../stores/sceneStore';
import VRMenu from './play/VRMenu.svelte';
Expand Down Expand Up @@ -257,6 +258,7 @@
tickAnimationPreview(); // Animation window: local transform preview (not synced)
tickMeshEdit(); // vertex handles follow the object if it moves (119)
updateLightHelpers();
updateColliderHelpers(); // CL-A A7: collider proxies follow their objects
if (!renderer.xr.isPresenting) updateEditorNavigation(delta, camera.current, $orbitControls);
});

Expand Down Expand Up @@ -353,6 +355,7 @@

onMount(() => {
startLightHelpers();
startColliderHelpers();
startEditorNavigation();
// tell peers our controllers are gone when the VR session ends
const onSessionEnd = () => {
Expand Down Expand Up @@ -416,7 +419,7 @@
if ($orbitControls) $orbitControls.enabled = false;
beginStroke($sculptObject);
const local = terrain.worldToLocal(hit.point.clone());
strokeMove($sculptObject, local.x, local.z);
strokeMove($sculptObject, local.x, local.z, 0.016, local.y); // y feeds the mesh brush
}
return;
}
Expand Down Expand Up @@ -447,13 +450,15 @@
setRayFromEvent(event);
const hit = terrain ? selectionRaycaster.intersectObject(terrain, false)[0] : null;
if (hit) {
showCursorAt(hit.point);
// mesh sculpt: orient the brush ring to the surface
const worldNormal = hit.face ? hit.face.normal.clone().transformDirection(terrain.matrixWorld) : null;
showCursorAt(hit.point, worldNormal);
if (sculptActive) {
const now = performance.now();
const dt = Math.min((now - lastSculptAt) / 1000, 0.1);
lastSculptAt = now;
const local = terrain.worldToLocal(hit.point.clone());
strokeMove($sculptObject, local.x, local.z, dt);
strokeMove($sculptObject, local.x, local.z, dt, local.y);
}
} else hideCursor();
if (sculptActive) return;
Expand Down Expand Up @@ -554,7 +559,8 @@
// face edit mode (135 desktop): a click highlights the face under it,
// and 163 attaches the transform gizmo to it (drag = move/rotate/scale)
if ($faceEditObject) {
const edited = $objectsGroup?.getObjectByProperty('uuid', $faceEditObject);
// A8: lookupEditable also finds the scene-root collider-edit proxy
const edited = lookupEditable($faceEditObject);
const hit = edited ? selectionRaycaster.intersectObject(edited, false)[0] : null;
const tri = hit && hit.faceIndex != null ? hit.faceIndex : -1;
highlightFaceByTriangle(tri);
Expand All @@ -564,7 +570,12 @@
if ($faceEditMulti) toggleFaceSelection(tri);
else autoApplyFaceOp();
}
attachFaceGizmo(); // 163: gizmo on the highlighted face (or detaches on a miss)
// B1 (inset fix): a seated MOVE gizmo intercepts the NEXT click (the
// dragging||axis guard above skips face dispatch), so click 2 of an
// armed inset/extrude DRAGGED the face instead. Only the Move op
// seats the gizmo; a miss still detaches it.
if ($faceEditOp === 'move' || tri < 0) attachFaceGizmo();
else detachFaceGizmo();
return;
}
// light pick-proxies select their light (lights have no raycastable geometry)
Expand Down
6 changes: 6 additions & 0 deletions src/components/editors/Nodes.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import ObjectPairNode from './nodes/ObjectPairNode.svelte';
import EffectNode from './nodes/EffectNode.svelte';
import OnClickNode from './nodes/OnClickNode.svelte';
import ColliderNode from './nodes/ColliderNode.svelte';
import VelocityNode from './nodes/VelocityNode.svelte';
import CounterNode from './nodes/CounterNode.svelte';
import FlowIONode from './nodes/FlowIONode.svelte';
import ObjectFlowNode from './nodes/ObjectFlowNode.svelte';
Expand Down Expand Up @@ -106,6 +108,10 @@
visibility: EffectNode,
onclick: OnClickNode,
onimpact: AnimationNode,
onenter: OnClickNode, // CL-C: same pulse card, sensor copy
onexit: OnClickNode,
collider: ColliderNode, // CL-C
velocity: VelocityNode, // CL-C
counter: CounterNode,
flowinput: FlowIONode,
flowoutput: FlowIONode,
Expand Down
9 changes: 9 additions & 0 deletions src/components/editors/nodes/AnimationNode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@
value={data[param.key] ?? spec.defaults[param.key]}
on:input={(e) => setNodeData(id, { [param.key]: +e.currentTarget.value })}
/>
{:else if param.kind === 'toggle'}
<!-- CL-C: boolean param (checkbox) -->
<span class="nodrag flex items-center gap-1.5">
<input
type="checkbox"
checked={!!(data[param.key] ?? spec.defaults[param.key])}
on:change={(e) => setNodeData(id, { [param.key]: e.currentTarget.checked })}
/>
</span>
{:else if param.kind === 'select'}
<select
class="nodrag"
Expand Down
61 changes: 61 additions & 0 deletions src/components/editors/nodes/ColliderNode.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script lang="ts">
import { Position, type NodeProps } from '@xyflow/svelte';
import Socket from './Socket.svelte';
import NodeWrapper from './NodeWrapper.svelte';
import { setNodeData } from '$lib/nodesHandler';
import { findNodeSpec } from '$lib/nodeCatalog';

// CL-C C1: collider override node — shape/scale/sensor are consumed by
// physics.js at sim start AND live (mid-sim rebuild, CL-A A2). The node
// WINS over the Inspector's collider pick (flow-overrides-Inspector, the
// mass precedent). Shape 'object' hulls the object wired into `source`.
type $$Props = NodeProps;
export let id: string;
export let data: any;
$: spec = findNodeSpec(data.type);
$: shapes = spec?.params?.find((p: any) => p.key === 'shape')?.options ?? ['box'];
</script>

<NodeWrapper type={data.type} label={data.label}>
<Socket kind="source" nodeType={data.type} position={Position.Right} />
<!-- the object to HULL when shape = object -->
<Socket kind="target" nodeType={data.type} position={Position.Left} id="source" style="top: 34px" />
<div class="flex w-full flex-col gap-1">
<label class="flex flex-col">
<span>shape</span>
<select
class="nodrag"
value={data.shape ?? 'box'}
on:change={(e) => setNodeData(id, { shape: e.currentTarget.value })}
>
{#each shapes as option}
<option value={option}>{option}</option>
{/each}
</select>
</label>
{#if (data.shape ?? 'box') === 'object'}
<p class="text-[10px] text-gray-400">◄ wire an Object Selector into source</p>
{/if}
<label class="flex flex-col">
<span class="flex justify-between"><span>scale</span><span>{(+(data.scale ?? 1)).toFixed(2)}</span></span>
<input
class="nodrag accent-[#ff4000]"
type="range"
min="0.25"
max="4"
step="0.05"
value={data.scale ?? 1}
on:input={(e) => setNodeData(id, { scale: +e.currentTarget.value })}
/>
</label>
<label class="nodrag flex items-center gap-1.5">
<input
type="checkbox"
checked={!!data.sensor}
on:change={(e) => setNodeData(id, { sensor: e.currentTarget.checked })}
/>
<span>sensor</span>
</label>
<p class="text-[10px] text-gray-400">overrides the Inspector collider; applies live</p>
</div>
</NodeWrapper>
11 changes: 9 additions & 2 deletions src/components/editors/nodes/OnClickNode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,22 @@
export let id: string;
export let data;
$: pulsing = $flowValues[id] === 1;
// CL-C: the same pulse card serves the sensor overlap triggers
const COPY: Record<string, [string, string]> = {
onclick: ['clicked!', 'connect to the object; pulses on click'],
onenter: ['entered!', 'pulses when something enters the sensor object'],
onexit: ['exited!', 'pulses when a sensor overlap ends']
};
$: copy = COPY[data.type] ?? COPY.onclick;
</script>

<NodeWrapper type={data.type} label={data.label}>
<Socket kind="source" nodeType={data.type} position={Position.Right} />
<div class="flex w-full flex-col gap-1">
<div class="flex items-center gap-2">
<span class="h-2.5 w-2.5 rounded-full" style="background: {pulsing ? '#22c55e' : '#374151'}"></span>
<span>{pulsing ? 'clicked!' : 'idle'}</span>
<span>{pulsing ? copy[0] : 'idle'}</span>
</div>
<p class="text-[10px] text-gray-400">connect to the object; pulses on click</p>
<p class="text-[10px] text-gray-400">{copy[1]}</p>
</div>
</NodeWrapper>
26 changes: 26 additions & 0 deletions src/components/editors/nodes/VelocityNode.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import { Position, type NodeProps } from '@xyflow/svelte';
import Socket from './Socket.svelte';
import NodeWrapper from './NodeWrapper.svelte';
import { flowValues } from '../../../stores/flowStore';

// CL-C C3: live speed (m/s) of the wired object — or the graph owner when
// unwired inside an object graph. LOCAL feed: exact-ish on the physics
// initiator (per-step write-back deltas), an ~10Hz move-delta APPROXIMATION
// on every other peer (broadcast-gated); 0 when nothing moves / no sim.
type $$Props = NodeProps;
export let id: string;
export let data: any;
$: speed = typeof $flowValues[id] === 'number' ? $flowValues[id] : 0;
</script>

<NodeWrapper type={data.type} label={data.label}>
<Socket kind="source" nodeType={data.type} position={Position.Right} />
<Socket kind="target" nodeType={data.type} position={Position.Left} id="target" style="top: 34px" />
<div class="flex w-full flex-col gap-1">
<span class="rounded bg-gray-900/70 px-1.5 py-0.5 font-mono text-[11px] text-primary-300"
>{speed.toFixed(2)} m/s</span
>
<p class="text-[10px] text-gray-400">object speed; approximate on non-sim peers</p>
</div>
</NodeWrapper>
Loading