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'} setNodeData(id, { shape: e.currentTarget.value })} + > + {#each shapes as option} + + {/each} + + + {#if (data.shape ?? 'box') === 'object'} +

◄ wire an Object Selector into source

+ {/if} + + +

overrides the Inspector collider; applies live

+ + diff --git a/src/components/editors/nodes/OnClickNode.svelte b/src/components/editors/nodes/OnClickNode.svelte index 7de28cbc..ce59773b 100644 --- a/src/components/editors/nodes/OnClickNode.svelte +++ b/src/components/editors/nodes/OnClickNode.svelte @@ -12,6 +12,13 @@ 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 = { + 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; @@ -19,8 +26,8 @@
- {pulsing ? 'clicked!' : 'idle'} + {pulsing ? copy[0] : 'idle'}
-

connect to the object; pulses on click

+

{copy[1]}

diff --git a/src/components/editors/nodes/VelocityNode.svelte b/src/components/editors/nodes/VelocityNode.svelte new file mode 100644 index 00000000..9c273773 --- /dev/null +++ b/src/components/editors/nodes/VelocityNode.svelte @@ -0,0 +1,26 @@ + + + + + +
+ {speed.toFixed(2)} m/s +

object speed; approximate on non-sim peers

+
+
diff --git a/src/components/menu/Inspector.svelte b/src/components/menu/Inspector.svelte index e17a5f82..2337729d 100644 --- a/src/components/menu/Inspector.svelte +++ b/src/components/menu/Inspector.svelte @@ -32,7 +32,10 @@ import { LIGHT_PARAMS, SHADOW_TYPES, SHADOW_SIZES, setShadowMapSize, cappedShadowSize } from '$lib/lightParams'; import { animatedObjects, setAnimationState } from '$lib/animatedImports'; import { moveObjectToGroup, selectObject } from '$lib/objectActions'; - import { listPhysicsObjects, enablePhysicsOnSelection } from '$lib/physics'; + import { listPhysicsObjects, enablePhysicsOnSelection, PHYSICS_MATERIALS, physicsShapeChanged } from '$lib/physics'; + import { sceneGravity, setSceneGravity, resetSceneGravity, DEFAULT_GRAVITY } from '$lib/scenePhysics'; + import { showColliders, colliderVizObjects, setColliderViz } from '$lib/colliderHelpers'; + import { enterColliderEdit } from '$lib/colliderEdit'; import { addParticlesPreset, updateObjectParticles, removeObjectParticles, burstObjectParticles } from '$lib/particleActions'; import { PARTICLE_PRESETS } from '$lib/particlePresets'; import { flowGraphs } from '../../stores/flowStore'; @@ -366,9 +369,29 @@ $selectedObject.userData.physics = next; recordEntry({ kind: 'props', uuid: $selectedObject.uuid, before: { physics: before }, after: { physics: next } }); $peers.send({ type: 'objectParameters', parameter: 'physics', uuid: $selectedObject.uuid, physics: next }); + objectsGroup.update((v) => v); // collider viz re-syncs from the poke + physicsShapeChanged($selectedObject.uuid); // CL-A A2: live mid-sim rebuild selectedObject.update((v) => v); } + /** CL-A A4: which material preset matches the current values (else 'custom') @param {any} p */ + function physicsMaterialOf(p) { + const r = p?.restitution ?? 0.3; + const f = p?.friction ?? 0.5; + const hit = Object.entries(PHYSICS_MATERIALS).find( + ([, m]) => Math.abs(m.restitution - r) < 0.001 && Math.abs(m.friction - f) < 0.001 + ); + return hit ? hit[0] : 'custom'; + } + + /** CL-A A5: toggle one freeze-axis flag @param {string} key @param {boolean} on */ + function setFreeze(key, on) { + const freeze = { ...($selectedObject.userData.physics?.freeze ?? {}) }; + if (on) freeze[key] = true; + else delete freeze[key]; + setPhysics({ freeze: Object.keys(freeze).length ? freeze : null }); + } + function sendName() { objectsGroup.update((value) => value); // refresh the object list $peers.send({ type: 'name', name: $selectedObject.name, uuid: $selectedObject.uuid }); @@ -404,8 +427,11 @@ } // ---- scene target (fog state is local, like the old scene panel) -------- + // fogColor is INITIALIZED: svelte 5.56 hard-errors on `bind:hex={undefined}` + // when the prop has a fallback (props_invalid_value) — undefined here + // CRASHED the whole scene drawer (pre-existing on release/1.1, deps bump) /** @type {any} */ - let fogColor = $state(); + let fogColor = $state('#ffffff'); /** @type {any} */ let fogNear = $state(0); /** @type {any} */ @@ -755,6 +781,7 @@ Local render mode (ambient occlusion + wireframe are desktop-only; not shown to peers).

Show light helpers + Show colliders — this device
{#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}
- -
- +
+ ⠿ {$colliderEditObject ? '🟩 Collider' : '🔷 Mesh'} - +
setMode('vertices')}>Vertices setMode('faces')}>Faces
+ + {#if mode === 'faces'} - +
- - + {#each GRANULARITIES as g (g.value)} + + {/each}
toggleFaceMulti()} + >Multi{$faceEditMulti && $faceEditSelectedTris.length ? ` (${$faceEditSelectedTris.length})` : ''} -
- {#each OPS as o} + + + + +
+ {#each OPS as o (o.op)} runOp(o.op)}>{o.label} {/each}
{:else} +
clearVertexSelection()}>Move + Create face {$vertexSelectionSize} sel
{/if} + + + meshEditWireframe.update((v) => !v)}>Wire + + {#if $colliderEditObject} + + + + + + {:else} + + {/if}
- + {#if mode === 'faces' && ($faceEditOp === 'extrude' || $faceEditOp === 'inset')} -
-