diff --git a/src/components/Scene.svelte b/src/components/Scene.svelte index 9065cea28..86983ecbb 100644 --- a/src/components/Scene.svelte +++ b/src/components/Scene.svelte @@ -72,6 +72,8 @@ import { measureMode, measureClick, measureHoverAt } from '$lib/measure'; import { pinsGroup, openAnnotation, showNotePins } from '$lib/annotationsHandler'; import { setParticleRoot } from '$lib/particleRuntime'; + import { setBackgroundRoot, tickBackgroundLayer } from '$lib/backgroundLayer'; // 65 M2 + import { tickFluidLayer } from '$lib/fluidLayer'; // 65 M6: smoke / ink (a look entry; the XR panel) import WaterLayer from './water/WaterLayer.svelte'; import { registerEditResumeSources } from '$lib/editResume'; import { sendPing } from '$lib/ping'; @@ -426,6 +428,8 @@ tickAnimatedMixers(); // imported clips run on the synced clock tickPackBehaviors(); // 33 P2: doors/lids/levers pose from their shared state (rest in Edit) tickSim($objectsGroup, camera.current, renderer, performance.now()); // 36-sim (after mixers: jiggle rides the posed bones) + tickBackgroundLayer(camera.current, renderer, performance.now()); // 65 M2: the shader background (inert without one) + tickFluidLayer(camera.current, renderer, performance.now()); // 65 M6: steps the XR smoke panel (the screen effect steps itself) tickAnimationPreview(); // Animation window: local transform preview (not synced) tickSequences(); // 48 S2: shots / clips / audio / events of running sequences tickMeshEdit(); // vertex handles follow the object if it moves (119) @@ -1830,6 +1834,9 @@ sceneObjects — they'd leak into GLTF sync). oncreate passes the ref DIRECTLY (the { ref } destructure trap, N1). --> setParticleRoot(ref)} /> + + setBackgroundRoot(ref)} /> diff --git a/src/components/editors/ShaderEditor.svelte b/src/components/editors/ShaderEditor.svelte index f6199f18f..ccede8631 100644 --- a/src/components/editors/ShaderEditor.svelte +++ b/src/components/editors/ShaderEditor.svelte @@ -40,7 +40,11 @@ shaderRefusalReason } from '$lib/shaderGraph'; import { beginShaderGesture, endShaderGesture } from '$lib/shaderSync'; - import { shaderNodeDefs, shaderNodeDef, SURFACE_NODE, POST_OUTPUT_NODE } from '$lib/shaderCatalog'; + import { shaderNodeDefs, shaderNodeDef, SURFACE_NODE, POST_OUTPUT_NODE, BACKGROUND_OUTPUT_NODE, stageForDomain } from '$lib/shaderCatalog'; + // 65 M2: the BACKGROUND domain — one document per scene, so its scope is fixed + import { BACKGROUND_KEY, removeBackground } from '$lib/backgroundLayer'; + import { backgroundPresets, loadBackgroundPresets, applyBackgroundPreset } from '$lib/backgroundPresets'; + import { BUILTIN_PRESET_KEY } from '$lib/backgroundPresetsCore'; // P4: the POST domain. The editor is one surface for two domains — same cards, same // palette, same pane menu — because a post graph IS a shader graph; what differs is // which nodes mean anything, which terminal node the graph ends at, and (since a post @@ -100,7 +104,8 @@ (def) => def.key !== SURFACE_NODE && def.key !== POST_OUTPUT_NODE && - (!def.stages || def.stages.includes(which === 'post' ? 'post' : 'fragment')) + def.key !== BACKGROUND_OUTPUT_NODE && + (!def.stages || def.stages.includes(/** @type {any} */ (stageForDomain(which)))) ); const catalog = $derived(catalogFor($shaderDomain)); const groups = $derived([...new Set(catalog.map((def) => def.group))]); @@ -111,6 +116,10 @@ // ---- scope: selection-driven in SURFACE, picked in POST --------------------- const selectedUuid = $derived($selectedObjects?.length === 1 ? $selectedObjects[0] : null); const isPost = $derived($shaderDomain === 'post'); + const isBg = $derived($shaderDomain === 'background'); + $effect(() => { + if (isBg) void loadBackgroundPresets(); + }); /** every post graph, re-derived off the store so a new one appears at once */ /** @param {any} _poke */ const graphsOf = (_poke) => postGraphKeys(); @@ -121,17 +130,21 @@ ? $activePostGraph : (postGraphs[0]?.key ?? '') ); - const scope = $derived(isPost ? postScope : (selectedUuid ?? SCENE_GRAPH_KEY)); + const scope = $derived(isBg ? BACKGROUND_KEY : isPost ? postScope : (selectedUuid ?? SCENE_GRAPH_KEY)); /** the navigator's list, split by domain (see its markup below) */ const treeDocuments = $derived( Object.fromEntries( - Object.entries($shaderGraphs).filter(([key]) => key.startsWith('post:') === isPost) + Object.entries($shaderGraphs).filter(([key]) => + isBg ? key === BACKGROUND_KEY : key !== BACKGROUND_KEY && key.startsWith('post:') === isPost + ) ) ); const doc = $derived(scope ? ($shaderGraphs[scope] ?? null) : null); const errors = $derived(scope ? ($shaderErrors[scope] ?? []) : []); const ownerName = $derived( - isPost + isBg + ? 'The background' + : isPost ? scope ? postGraphName(scope) : 'No post effect' @@ -140,7 +153,9 @@ : $objectsGroup?.getObjectByProperty('uuid', scope)?.name || 'This object' ); const scopeLabel = $derived( - isPost + isBg + ? 'Scene background' + : isPost ? ownerName + ' — post effect' : scope === SCENE_GRAPH_KEY ? 'Scene default material' @@ -280,6 +295,10 @@ // ---- actions --------------------------------------------------------------- function createGraph() { + if (isBg) { + void applyBackgroundPreset(BUILTIN_PRESET_KEY); + return; + } if (isPost) { // in the post half "create" MINTS a document (there is no object to attach one // to), and the new one becomes the scope so you are looking at what you made @@ -311,6 +330,10 @@ } function removeGraph() { + if (isBg) { + removeBackground(); + return; + } if (isPost) { if (scope) deletePostGraph(scope); activePostGraph.set(null); @@ -425,7 +448,7 @@ { label: 'Disconnect all', action: () => disconnectNode(id) }, // the Surface output is the graph's terminal — there is exactly one, and a // graph without it cannot compile at all - ...(node.type === SURFACE_NODE + ...(node.type === SURFACE_NODE || node.type === BACKGROUND_OUTPUT_NODE ? [] : [{ label: 'Delete node', danger: true, action: () => deleteNode(id) }]) ] @@ -653,7 +676,7 @@ @@ -664,6 +687,13 @@ title="Post effects: a fragment over the finished frame, added to the scene look" onclick={() => shaderDomain.set('post')}>Post + {#if isPost && postGraphs.length} @@ -712,8 +742,8 @@
@@ -781,16 +811,29 @@

- {#if isPost} + {#if isBg} + The scene has no background yet + {:else if isPost} No post effect to edit yet {:else} {ownerName} has no shader yet {/if}

- {#if isPost} + {#if isBg} +
+ {#each $backgroundPresets.presets as preset (preset.key)} + + {/each} +
+

+ A background draws behind the scene, or on a plane — Configure Scene ▸ Background +

+ {:else if isPost}
@@ -1002,6 +1045,8 @@ {doc.nodes.length} nodes · {doc.edges.length} wires · {doc.backend}

Replicates to peers · saved with the scene

+ {:else if isBg} +

Pick a preset to start a background, then take it apart here.

{:else if isPost}

Create a post effect, then add it to a look in Configure Scene ▸ diff --git a/src/components/fx/ParticleMotionRows.svelte b/src/components/fx/ParticleMotionRows.svelte index 6e002b001..ce8a71e18 100644 --- a/src/components/fx/ParticleMotionRows.svelte +++ b/src/components/fx/ParticleMotionRows.svelte @@ -20,7 +20,7 @@ ]; -

+
@@ -45,6 +45,16 @@ In Local space the ribbon rides the object. Set Space to World for a trail behind it.

{/if} + + set({ flow: v })} /> + {#if (p.flow ?? 0) > 0} + set({ flowScale: v })} /> + set({ flowSpeed: v })} /> + {/if} {#if world} set({ inherit: v })} /> diff --git a/src/components/menu/Inspector.svelte b/src/components/menu/Inspector.svelte index e37d9de07..46ceda7bc 100644 --- a/src/components/menu/Inspector.svelte +++ b/src/components/menu/Inspector.svelte @@ -26,6 +26,7 @@ import MenuButton from './inspector/MenuButton.svelte'; import InsToggle from './inspector/InsToggle.svelte'; import SceneQualitySection from './inspector/SceneQualitySection.svelte'; // 42 I12 + import BackgroundLayerSection from './inspector/BackgroundLayerSection.svelte'; // 65 M2 import ArrangeSection from './inspector/ArrangeSection.svelte'; // 47 M2 import TerrainPaintSection from './inspector/TerrainPaintSection.svelte'; // 47 T1/T2 import MeasureSection from './inspector/MeasureSection.svelte'; // 48 B2 @@ -2122,6 +2123,7 @@ Open the shader editor +