diff --git a/NODES.md b/NODES.md
index 04cd727b..213f6dbd 100644
--- a/NODES.md
+++ b/NODES.md
@@ -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--` WITHOUT handles, so
diff --git a/src/App.svelte b/src/App.svelte
index a921938a..80258a75 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -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 }
})
}
})
diff --git a/src/components/Scene.svelte b/src/components/Scene.svelte
index 39b78409..41e97e48 100644
--- a/src/components/Scene.svelte
+++ b/src/components/Scene.svelte
@@ -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';
@@ -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';
@@ -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);
});
@@ -353,6 +355,7 @@
onMount(() => {
startLightHelpers();
+ startColliderHelpers();
startEditorNavigation();
// tell peers our controllers are gone when the VR session ends
const onSessionEnd = () => {
@@ -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;
}
@@ -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;
@@ -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);
@@ -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)
diff --git a/src/components/editors/Nodes.svelte b/src/components/editors/Nodes.svelte
index a882f700..3965f9f3 100644
--- a/src/components/editors/Nodes.svelte
+++ b/src/components/editors/Nodes.svelte
@@ -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';
@@ -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,
diff --git a/src/components/editors/nodes/AnimationNode.svelte b/src/components/editors/nodes/AnimationNode.svelte
index 33536956..b4bc83b6 100644
--- a/src/components/editors/nodes/AnimationNode.svelte
+++ b/src/components/editors/nodes/AnimationNode.svelte
@@ -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'}
+
+
+ setNodeData(id, { [param.key]: e.currentTarget.checked })}
+ />
+
{:else if param.kind === 'select'}
Show light helpers
+ Show colliders — this device
Camera lens
{#each LENS_PRESETS as p (p.label)}
@@ -814,6 +841,20 @@
+
+ setSceneGravity(v)} />
+
+
+
+
+ Shared with everyone and applies to running simulations live.
+
{#if physicsRows.length === 0}
@@ -1515,6 +1556,24 @@
setPhysics({ mass: v })} />
{/if}
+
+ Material
+ {
+ const m = PHYSICS_MATERIALS[v];
+ if (m) setPhysics({ restitution: m.restitution, friction: m.friction });
+ }}
+ />
+
setPhysics({ restitution: v })} />
setPhysics({ collider: v })}
+ onchange={(/** @type {any} */ v) => {
+ // A8: picking Custom opens the edit session; Done writes the verts
+ if (v === 'custom') enterColliderEdit($selectedObject.uuid);
+ else setPhysics({ collider: v });
+ }}
/>
+ {#if ($selectedObject.userData.physics?.collider ?? 'box') === 'custom'}
+
+ {/if}
+
+
+ {#if ($selectedObject.userData.physics?.mode ?? 'auto') === 'dynamic'}
+
+
+ Lock rotation
+ {#each [['rx', 'X'], ['ry', 'Y'], ['rz', 'Z']] as [key, label] (key)}
+
+ {/each}
+
+
+ Lock position
+ {#each [['px', 'X'], ['py', 'Y'], ['pz', 'Z']] as [key, label] (key)}
+
+ {/each}
+
+ {/if}
+
+
Dynamic bodies fall and collide when a simulation runs; flow Mass/Bounciness/Friction nodes override these.
diff --git a/src/components/menu/MeshEditPopup.svelte b/src/components/menu/MeshEditPopup.svelte
index e13451ba..7dfef4b8 100644
--- a/src/components/menu/MeshEditPopup.svelte
+++ b/src/components/menu/MeshEditPopup.svelte
@@ -1,18 +1,18 @@
-
-
+
{#if active}