beam: add zoom speed control
This commit is contained in:
parent
1569ed9c23
commit
6d65b2ba41
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 недоступно" });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue