beam: add zoom speed control

This commit is contained in:
CODEX 2026-06-04 23:22:00 +03:00
parent 1569ed9c23
commit 6d65b2ba41
4 changed files with 175 additions and 19 deletions

View File

@ -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;

View File

@ -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 недоступно" });
});

View File

@ -87,7 +87,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./dcViewer.css?v=6">
<link rel="stylesheet" href="./dcViewer.css?v=7">
</head>
<body>
<header>
@ -129,18 +129,19 @@
<h3>Инспектор</h3>
<div id="treeContainer" class="tree"></div>
<div class="inspector-controls">
<div class="inspector-control-row" id="displayModeControl">
<div class="inspector-control-row inspector-control-row--half" id="displayModeControl">
<div class="inspector-control-label">Режим отображения</div>
<div class="inspector-select-host" id="displayModeSelect"></div>
</div>
<div class="inspector-control-row" id="lightingModeControl">
<div class="inspector-control-row inspector-control-row--half" id="lightingModeControl">
<div class="inspector-control-label">Свет</div>
<div class="inspector-select-host" id="lightingModeSelect"></div>
</div>
<div class="inspector-control-row" id="navigationAxisControl">
<div class="inspector-control-row inspector-control-row--full" id="navigationAxisControl">
<div class="inspector-control-label">Ось навигации</div>
<div class="inspector-select-host" id="navigationAxisSelect"></div>
</div>
<div class="inspector-control-row inspector-control-row--full inspector-control-row--slider" id="zoomSpeedControl"></div>
</div>
</div>
<div class="panel-section" id="projectNameSection">
@ -238,6 +239,6 @@
<input id="fileInput" class="hidden" type="file"
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
<script type="module" src="./dcViewer.js?v=6"></script>
<script type="module" src="./dcViewer.js?v=7"></script>
</body>
</html>

View File

@ -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;
}