diff --git a/packages/editor/src/components/ui/controls/slider-control.tsx b/packages/editor/src/components/ui/controls/slider-control.tsx index 1746cca3..937f637d 100644 --- a/packages/editor/src/components/ui/controls/slider-control.tsx +++ b/packages/editor/src/components/ui/controls/slider-control.tsx @@ -21,6 +21,20 @@ function stepPrecision(s: number): number { return Math.max(0, Math.ceil(-Math.log10(s))) } +function getAdjustedStep( + baseStep: number, + modifiers: { + shiftKey?: boolean + metaKey?: boolean + ctrlKey?: boolean + altKey?: boolean + }, +): number { + if (modifiers.shiftKey) return baseStep * 10 + if (modifiers.metaKey || modifiers.ctrlKey || modifiers.altKey) return baseStep * 0.1 + return baseStep +} + export function SliderControl({ label, value, @@ -58,16 +72,14 @@ export function SliderControl({ if (isEditing) return e.preventDefault() const direction = e.deltaY < 0 ? 1 : -1 - let s = step - if (e.shiftKey) s = step * 10 - else if (e.altKey) s = step * 0.1 + const s = getAdjustedStep(step, e) const newValue = clamp(valueRef.current + direction * s) const final = Number.parseFloat(newValue.toFixed(stepPrecision(s))) if (final !== valueRef.current) onChange(final) } el.addEventListener('wheel', handleWheel, { passive: false }) return () => el.removeEventListener('wheel', handleWheel) - }, [isEditing, step, clamp, onChange, precision]) + }, [isEditing, step, clamp, onChange]) // Arrow key support while hovered useEffect(() => { @@ -78,9 +90,7 @@ export function SliderControl({ else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') direction = -1 if (direction !== 0) { e.preventDefault() - let s = step - if (e.shiftKey) s = step * 10 - else if (e.metaKey || e.ctrlKey) s = step * 0.1 + const s = getAdjustedStep(step, e) const newValue = clamp(valueRef.current + direction * s) const final = Number.parseFloat(newValue.toFixed(stepPrecision(s))) if (final !== valueRef.current) onChange(final) @@ -88,7 +98,7 @@ export function SliderControl({ } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [isHovered, isEditing, step, clamp, onChange, precision]) + }, [isHovered, isEditing, step, clamp, onChange]) const handleLabelPointerDown = useCallback( (e: React.PointerEvent) => { @@ -107,16 +117,14 @@ export function SliderControl({ if (!dragRef.current) return const { startX, startValue } = dragRef.current const dx = e.clientX - startX - let s = step - if (e.shiftKey) s = step * 10 - else if (e.metaKey || e.ctrlKey) s = step * 0.1 + const s = getAdjustedStep(step, e) // 4 px per step at default sensitivity const newValue = clamp( Number.parseFloat((startValue + (dx / 4) * s).toFixed(stepPrecision(s))), ) onChange(newValue) }, - [step, precision, clamp, onChange], + [step, clamp, onChange], ) const handleLabelPointerUp = useCallback( @@ -163,12 +171,18 @@ export function SliderControl({ setIsEditing(false) } else if (e.key === 'ArrowUp') { e.preventDefault() - const newV = clamp(value + step) + const adjustedStep = getAdjustedStep(step, e) + const newV = clamp( + Number.parseFloat((value + adjustedStep).toFixed(stepPrecision(adjustedStep))), + ) onChange(newV) setInputValue(newV.toFixed(precision)) } else if (e.key === 'ArrowDown') { e.preventDefault() - const newV = clamp(value - step) + const adjustedStep = getAdjustedStep(step, e) + const newV = clamp( + Number.parseFloat((value - adjustedStep).toFixed(stepPrecision(adjustedStep))), + ) onChange(newV) setInputValue(newV.toFixed(precision)) } diff --git a/packages/editor/src/components/ui/panels/wall-panel.tsx b/packages/editor/src/components/ui/panels/wall-panel.tsx index 96cd697b..65f2d7d8 100755 --- a/packages/editor/src/components/ui/panels/wall-panel.tsx +++ b/packages/editor/src/components/ui/panels/wall-panel.tsx @@ -228,7 +228,7 @@ export function WallPanel() { min={-Math.max(0.01, maxCurveOffset)} onChange={(v) => handleUpdate({ curveOffset: normalizeWallCurveOffset(node, v) })} precision={2} - step={0.01} + step={0.1} unit="m" value={Math.round(curveOffset * 100) / 100} /> diff --git a/packages/viewer/src/components/renderers/slab/slab-renderer.tsx b/packages/viewer/src/components/renderers/slab/slab-renderer.tsx index 537433fe..f491cef2 100644 --- a/packages/viewer/src/components/renderers/slab/slab-renderer.tsx +++ b/packages/viewer/src/components/renderers/slab/slab-renderer.tsx @@ -1,11 +1,11 @@ -import { type SlabNode, useRegistry } from '@pascal-app/core' +import { getMaterialPresetByRef, type SlabNode, useRegistry } from '@pascal-app/core' import { useEffect, useMemo, useRef } from 'react' import * as THREE from 'three' import type { Mesh } from 'three' import { useNodeEvents } from '../../../hooks/use-node-events' import { + applyMaterialPresetToMaterials, createMaterial, - createMaterialFromPresetRef, DEFAULT_SLAB_MATERIAL, } from '../../../lib/materials' @@ -17,9 +17,18 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => { const handlers = useNodeEvents(node, 'slab') const material = useMemo(() => { - const presetMaterial = createMaterialFromPresetRef(node.materialPreset) - const sourceMaterial = presetMaterial ?? (node.material ? createMaterial(node.material) : DEFAULT_SLAB_MATERIAL) - const slabMaterial = sourceMaterial.clone() + const preset = getMaterialPresetByRef(node.materialPreset) + const slabMaterial = preset + ? new THREE.MeshStandardMaterial() + : node.material + ? createMaterial(node.material).clone() + : DEFAULT_SLAB_MATERIAL.clone() + + if (preset) { + // Apply the preset to the slab-owned material so async texture loads update + // the instance we actually render after refresh as well. + applyMaterialPresetToMaterials(slabMaterial, preset) + } // Slabs participate in the WebGPU MRT scene pass. Keeping them opaque avoids // pipeline variants that can fail when geometry is regenerated while a