From 6d65b2ba4111b00cb946d09474b75b2e5c682d64 Mon Sep 17 00:00:00 2001 From: CODEX Date: Thu, 4 Jun 2026 23:22:00 +0300 Subject: [PATCH] beam: add zoom speed control --- frontend/dcViewer.css | 102 ++++++++++++++++++++++++++--- frontend/dcViewer.js | 53 ++++++++++++++- frontend/index.html | 11 ++-- frontend/src/ui/controls/slider.js | 28 +++++++- 4 files changed, 175 insertions(+), 19 deletions(-) diff --git a/frontend/dcViewer.css b/frontend/dcViewer.css index 91de159..8427200 100644 --- a/frontend/dcViewer.css +++ b/frontend/dcViewer.css @@ -604,24 +604,32 @@ header { .inspector-controls { display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 10px; } .inspector-control-row { - display: grid; - grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr); - gap: 10px; - align-items: center; - min-height: 42px; + display: flex; + min-width: 0; + flex-direction: column; + gap: 6px; +} + +.inspector-control-row--full { + grid-column: 1 / -1; +} + +.inspector-control-row--slider { + margin-top: 2px; } .inspector-control-label { min-width: 0; color: var(--muted); - font-size: 12px; + font-size: 11px; font-weight: 760; - line-height: 1.15; + line-height: 1.2; } .inspector-select-host { @@ -636,7 +644,7 @@ header { .nodedc-select__control { display: grid; - grid-template-columns: minmax(0, 1fr) 42px; + grid-template-columns: minmax(0, 1fr) 40px; gap: 8px; align-items: center; min-width: 0; @@ -644,7 +652,7 @@ header { .nodedc-select__value, .nodedc-select__toggle { - height: 42px; + height: 40px; border: 0; border-radius: 14px; background: var(--nodedc-glass-control-bg); @@ -668,6 +676,20 @@ header { cursor: pointer; } +.inspector-control-row--half .nodedc-select__control { + grid-template-columns: minmax(0, 1fr) 36px; + gap: 6px; +} + +.inspector-control-row--half .nodedc-select__value, +.inspector-control-row--half .nodedc-select__toggle { + height: 40px; +} + +.inspector-control-row--half .nodedc-select__value { + padding: 0 10px; +} + .nodedc-select__toggle { display: grid; place-items: center; @@ -1106,6 +1128,13 @@ header { font-size: 13px; } +.slider-value { + color: var(--text); + font-size: 12px; + font-weight: 760; + line-height: 1; +} + .slider-input { width: 100%; appearance: none; @@ -1137,6 +1166,61 @@ header { box-shadow: 0 0 6px rgba(0, 0, 0, 0.25); } +.inspector-slider.slider-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: + "label value" + "input input"; + gap: 8px; + align-items: center; + padding: 0; +} + +.inspector-slider .slider-label { + grid-area: label; + min-width: 0; + color: var(--muted); + font-size: 11px; + font-weight: 760; + line-height: 1.2; +} + +.inspector-slider .slider-value { + grid-area: value; + min-width: 38px; + color: rgba(245, 245, 250, 0.82); + text-align: right; +} + +.inspector-slider .slider-input { + grid-area: input; + height: 40px; + border-radius: 14px; + background: linear-gradient( + 90deg, + var(--modal-btn-primary, var(--accent)) var(--slider-percent, 0%), + var(--nodedc-glass-control-bg) var(--slider-percent, 0%) + ); + box-shadow: var(--nodedc-glass-control-shadow); +} + +.inspector-slider .slider-input::-webkit-slider-thumb { + width: 20px; + height: 20px; + background: var(--modal-btn-primary, var(--accent)); + border: 0; + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.32); +} + +.inspector-slider .slider-input::-moz-range-thumb { + width: 20px; + height: 20px; + background: var(--modal-btn-primary, var(--accent)); + border: 0; + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.32); +} + .settings-panel input[type="color"] { width: 100%; height: 26px; diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 85f6bb0..714b960 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -7,6 +7,7 @@ import { createTemplatesMenu } from "./src/ui/templatesMenu.js"; import { createSettingsMenu } from "./src/ui/settingsMenu.js"; import { createMeasurementModal } from "./src/ui/measurementModal.js"; import { createGlassSelect } from "./src/ui/glassSelect.js"; +import { createSliderControl } from "./src/ui/controls/slider.js"; import "./theme.js"; import { TransformGizmo } from "./transformGizmo.js"; import { createObjectModal } from "./src/ui/objectModal.js"; @@ -42,6 +43,7 @@ const lightingModeControl = document.getElementById("lightingModeControl"); const lightingModeSelectHost = document.getElementById("lightingModeSelect"); const navigationAxisControl = document.getElementById("navigationAxisControl"); const navigationAxisSelectHost = document.getElementById("navigationAxisSelect"); +const zoomSpeedControl = document.getElementById("zoomSpeedControl"); const logList = document.getElementById("logList"); const loaderEl = document.getElementById("loader"); const navCubeCanvas = document.getElementById("navCube"); @@ -216,9 +218,11 @@ const sceneModels = {}; // modelId -> SceneModel let activeDisplayMode = "source"; let activeLightingMode = "ambient"; let activeNavigationAxis = "auto"; +let activeZoomSpeed = 20; let displayModeSelect = null; let lightingModeSelect = null; let navigationAxisSelect = null; +let zoomSpeedSlider = null; let defaultCameraWorldAxis = null; let defaultCameraUp = null; let currentSelection = null; // { modelId, objectId } @@ -244,27 +248,42 @@ const treeModelIds = new Set(); const CAMERA_CONTROL_SETTINGS = { keyboardDollyRate: 45, - mouseWheelDollyRate: 20, dollyInertia: 0, dollyMinSpeed: 0.008, dollyProximityThreshold: 90, }; +const normalizeZoomSpeed = (value) => { + const numeric = Number(value); + if (!Number.isFinite(numeric)) return 20; + return Math.min(100, Math.max(5, Math.round(numeric))); +}; + const configureCameraControl = () => { const control = viewer?.cameraControl; if (!control) return; + activeZoomSpeed = normalizeZoomSpeed(activeZoomSpeed); control.panRightClick = false; control.followPointer = true; control.doublePickFlyTo = false; control.smartPivot = true; control.zoomOnMouseWheel = true; control.keyboardDollyRate = CAMERA_CONTROL_SETTINGS.keyboardDollyRate; - control.mouseWheelDollyRate = CAMERA_CONTROL_SETTINGS.mouseWheelDollyRate; + control.mouseWheelDollyRate = activeZoomSpeed; control.dollyInertia = CAMERA_CONTROL_SETTINGS.dollyInertia; control.dollyMinSpeed = CAMERA_CONTROL_SETTINGS.dollyMinSpeed; control.dollyProximityThreshold = CAMERA_CONTROL_SETTINGS.dollyProximityThreshold; }; +const setZoomSpeed = (value, { announce = false } = {}) => { + activeZoomSpeed = normalizeZoomSpeed(value); + zoomSpeedSlider?.setValue?.(activeZoomSpeed); + configureCameraControl(); + if (announce) { + setStatus(`Скорость зума: ${activeZoomSpeed}%`); + } +}; + const ensureCameraControlActive = () => { if (!viewer || !viewer.cameraControl) return; configureCameraControl(); @@ -2205,6 +2224,7 @@ const buildProjectSnapshot = async (name) => { displayMode: activeDisplayMode, lightingMode: activeLightingMode, navigationAxis: activeNavigationAxis, + zoomSpeed: activeZoomSpeed, selection: { ids: lastSelection.ids || [], modelId: lastSelection.modelId || null, @@ -2267,7 +2287,8 @@ const buildModelViewerSettings = () => ({ edges: !!toggleEdges?.checked, displayMode: activeDisplayMode, lightingMode: activeLightingMode, - navigationAxis: activeNavigationAxis + navigationAxis: activeNavigationAxis, + zoomSpeed: activeZoomSpeed } : {}, design: settingsMenu?.getState ? settingsMenu.getState() : {} }); @@ -2307,6 +2328,9 @@ const applyModelViewerSettings = (settings, { applyCamera = true, applyDesign = activeNavigationAxis = viewerState.navigationAxis; applyNavigationAxis({ resetAuto: false }); } + if (viewerState.zoomSpeed !== undefined) { + setZoomSpeed(viewerState.zoomSpeed); + } if (applyCamera) { applyCameraSnapshot(viewerState.camera); } @@ -2391,6 +2415,9 @@ const applyViewerState = (state) => { activeNavigationAxis = state.navigationAxis; applyNavigationAxis({ resetAuto: false }); } + if (state.zoomSpeed !== undefined) { + setZoomSpeed(state.zoomSpeed); + } if (state.transforms && typeof state.transforms === "object") { Object.keys(transformStore).forEach((k) => delete transformStore[k]); Object.assign(transformStore, safeClone(state.transforms)); @@ -2971,6 +2998,9 @@ const initViewer = async () => { activeNavigationAxis = params.get("navigationAxis") || params.get("axis") || "auto"; applyNavigationAxis({ resetAuto: false }); } + if (params.has("zoomSpeed")) { + setZoomSpeed(params.get("zoomSpeed")); + } if (modelSettings?.viewerState) { modelSettings = { @@ -2980,6 +3010,7 @@ const initViewer = async () => { displayMode: activeDisplayMode, lightingMode: activeLightingMode, navigationAxis: activeNavigationAxis, + zoomSpeed: activeZoomSpeed, edges: params.has("edges") ? params.get("edges") !== "false" : modelSettings.viewerState.edges } }; @@ -3327,6 +3358,22 @@ const loadProjectSnapshot = async (project) => { }); setNavigationAxisControlState(); } + if (zoomSpeedControl) { + zoomSpeedSlider = createSliderControl({ + label: "Скорость зума", + value: activeZoomSpeed, + min: 5, + max: 100, + step: 1, + className: "inspector-slider", + showValue: true, + formatValue: (value) => `${Math.round(value)}%`, + onChange: (value) => { + setZoomSpeed(value); + }, + }); + zoomSpeedControl.replaceChildren(zoomSpeedSlider); + } loadProjectsList().catch(() => { templatesMenu?.setData({ templates: templateFallbacks, projects: [], error: "API недоступно" }); }); diff --git a/frontend/index.html b/frontend/index.html index 1f7f7a3..49e183f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -87,7 +87,7 @@ - +
@@ -129,18 +129,19 @@

Инспектор

-
+
Режим отображения
-
+
Свет
-
@@ -238,6 +239,6 @@ - + diff --git a/frontend/src/ui/controls/slider.js b/frontend/src/ui/controls/slider.js index c767e79..06314d5 100644 --- a/frontend/src/ui/controls/slider.js +++ b/frontend/src/ui/controls/slider.js @@ -1,6 +1,16 @@ -export function createSliderControl({ label = "", value = 0, min = 0, max = 100, step = 1, onChange }) { +export function createSliderControl({ + label = "", + value = 0, + min = 0, + max = 100, + step = 1, + className = "", + showValue = false, + formatValue = (val) => String(val), + onChange +}) { const row = document.createElement("div"); - row.className = "slider-row"; + row.className = ["slider-row", className].filter(Boolean).join(" "); const lbl = document.createElement("label"); lbl.textContent = label; @@ -14,9 +24,15 @@ export function createSliderControl({ label = "", value = 0, min = 0, max = 100, input.step = step; input.value = value; + const valueEl = document.createElement("div"); + valueEl.className = "slider-value"; + const updateFill = () => { const percent = ((input.value - input.min) / (input.max - input.min)) * 100; input.style.setProperty("--slider-percent", `${percent}%`); + if (showValue) { + valueEl.textContent = formatValue(Number(input.value)); + } }; input.addEventListener("input", () => { @@ -29,5 +45,13 @@ export function createSliderControl({ label = "", value = 0, min = 0, max = 100, row.appendChild(lbl); row.appendChild(input); + if (showValue) { + row.appendChild(valueEl); + } + row.setValue = (nextValue) => { + input.value = nextValue; + updateFill(); + }; + row.getValue = () => Number(input.value); return row; }