diff --git a/frontend/dcViewer.css b/frontend/dcViewer.css index 39323ac..b4213cf 100644 --- a/frontend/dcViewer.css +++ b/frontend/dcViewer.css @@ -624,6 +624,10 @@ header { margin-top: 2px; } +.inspector-color-control.hidden { + display: none; +} + .inspector-control-label { min-width: 0; color: var(--muted); @@ -636,6 +640,55 @@ header { min-width: 0; } +.inspector-color-row { + display: grid; + grid-template-columns: 40px minmax(0, 1fr); + align-items: center; + height: 40px; + min-width: 0; + border-radius: 14px; + background: var(--nodedc-glass-control-bg); + box-shadow: var(--nodedc-glass-control-shadow); + overflow: hidden; +} + +.inspector-color-row input[type="color"] { + width: 24px; + height: 24px; + margin-left: 12px; + border: 0; + border-radius: 50%; + background: transparent; + cursor: pointer; + padding: 0; + appearance: none; + -webkit-appearance: none; +} + +.inspector-color-row input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +.inspector-color-row input[type="color"]::-webkit-color-swatch { + border: 0; + border-radius: 50%; +} + +.inspector-color-row input[type="text"] { + min-width: 0; + width: 100%; + height: 100%; + border: 0; + background: transparent; + color: rgba(245, 245, 250, 0.82); + font: inherit; + font-size: 12px; + font-weight: 760; + outline: none; + padding: 0 14px 0 8px; + text-align: right; +} + .nodedc-select { position: relative; min-width: 0; diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index d83f32c..3e935d2 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -41,6 +41,9 @@ const displayModeControl = document.getElementById("displayModeControl"); const displayModeSelectHost = document.getElementById("displayModeSelect"); const lightingModeControl = document.getElementById("lightingModeControl"); const lightingModeSelectHost = document.getElementById("lightingModeSelect"); +const clayColorControl = document.getElementById("clayColorControl"); +const clayColorInput = document.getElementById("clayColorInput"); +const clayColorHexInput = document.getElementById("clayColorHexInput"); const navigationAxisControl = document.getElementById("navigationAxisControl"); const navigationAxisSelectHost = document.getElementById("navigationAxisSelect"); const zoomSpeedControl = document.getElementById("zoomSpeedControl"); @@ -180,6 +183,8 @@ const LIGHTING_MODE_OPTIONS = [ { value: "technical", label: "Тех." }, ]; +const DEFAULT_CLAY_COLOR = "#dbdbd1"; + const NAVIGATION_AXIS_PRESETS = { "y-up": { worldAxis: [1, 0, 0, 0, 1, 0, 0, 0, 1], @@ -220,6 +225,7 @@ let activeDisplayMode = "source"; let activeLightingMode = "ambient"; let activeNavigationAxis = "auto"; let activeZoomSpeed = 20; +let activeClayColor = DEFAULT_CLAY_COLOR; let displayModeSelect = null; let lightingModeSelect = null; let navigationAxisSelect = null; @@ -283,6 +289,7 @@ const configureCameraControl = () => { if (!control) return; activeZoomSpeed = normalizeZoomSpeed(activeZoomSpeed); configureCameraPanMap(control); + control.pointerEnabled = true; control.followPointer = true; control.doublePickFlyTo = false; control.smartPivot = true; @@ -306,6 +313,9 @@ const setZoomSpeed = (value, { announce = false } = {}) => { const ensureCameraControlActive = () => { if (!viewer || !viewer.cameraControl) return; configureCameraControl(); + if (viewer.cameraControl.pointerEnabled === false) { + viewer.cameraControl.pointerEnabled = true; + } if (viewer.cameraControl.enabled === false) { viewer.cameraControl.enabled = true; } @@ -316,9 +326,21 @@ const ensureCameraControlActive = () => { const resetCameraControl = () => { if (!viewer?.cameraControl) return; - // Toggle active to drop any stuck drag state without adding extra loops + // pointerEnabled setter calls xeokit's internal _reset(), which clears stuck drag state. + viewer.cameraControl.pointerEnabled = false; + viewer.cameraControl.pointerEnabled = true; viewer.cameraControl.active = false; viewer.cameraControl.active = true; + const canvas = viewer.scene?.canvas?.canvas; + if (canvas) { + ["mouseup", "pointerup"].forEach((type) => { + try { + canvas.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, button: 0, buttons: 0 })); + } catch (_) { + // noop + } + }); + } }; const rearmCameraControl = () => { @@ -365,6 +387,7 @@ const normalizeNavigationAxis = (axis) => ( const setDisplayModeControlState = () => { activeDisplayMode = normalizeDisplayMode(activeDisplayMode); displayModeSelect?.setValue(activeDisplayMode); + updateClayColorControlVisibility(); }; const setLightingModeControlState = () => { @@ -420,7 +443,7 @@ const hasModelSettingsTarget = () => !!getModelSettingsSrc(getPrimaryModelEntry( const getDisplayModePreset = (mode) => { switch (mode) { case "clay": - return { colorize: [0.86, 0.86, 0.82], opacity: 1, edges: false, xrayed: false }; + return { colorize: hexToRgb01(activeClayColor), opacity: 1, edges: false, xrayed: false }; case "white": return { colorize: [1, 1, 1], opacity: 1, edges: false, xrayed: false }; case "contrast": @@ -797,6 +820,37 @@ const hexToRgb01 = (hex) => { return [(num >> 16 & 255) / 255, (num >> 8 & 255) / 255, (num & 255) / 255]; }; +const normalizeHexColor = (value, fallback = DEFAULT_CLAY_COLOR) => { + const raw = typeof value === "string" ? value.trim() : ""; + const withHash = raw.startsWith("#") ? raw : `#${raw}`; + if (/^#([0-9a-fA-F]{3})$/.test(withHash)) { + return `#${withHash.slice(1).split("").map((char) => char + char).join("")}`.toLowerCase(); + } + if (/^#([0-9a-fA-F]{6})$/.test(withHash)) { + return withHash.toLowerCase(); + } + return fallback; +}; + +const syncClayColorInputs = () => { + const color = normalizeHexColor(activeClayColor); + if (clayColorInput) clayColorInput.value = color; + if (clayColorHexInput) clayColorHexInput.value = color; +}; + +const updateClayColorControlVisibility = () => { + clayColorControl?.classList.toggle("hidden", activeDisplayMode !== "clay"); + syncClayColorInputs(); +}; + +const setClayColor = (value, { apply = true } = {}) => { + activeClayColor = normalizeHexColor(value); + syncClayColorInputs(); + if (apply && activeDisplayMode === "clay") { + applyDisplayMode(); + } +}; + const measureThemeFromCSS = () => { const pick = (name, fallback) => { const v = cssValue(name); @@ -2341,6 +2395,7 @@ const buildProjectSnapshot = async (name) => { edges: !!toggleEdges?.checked, addMode: !!addMode?.checked, displayMode: activeDisplayMode, + clayColor: activeClayColor, lightingMode: activeLightingMode, navigationAxis: activeNavigationAxis, zoomSpeed: activeZoomSpeed, @@ -2405,6 +2460,7 @@ const buildModelViewerSettings = () => ({ transparent: !!toggleTransparent?.checked, edges: !!toggleEdges?.checked, displayMode: activeDisplayMode, + clayColor: activeClayColor, lightingMode: activeLightingMode, navigationAxis: activeNavigationAxis, zoomSpeed: activeZoomSpeed @@ -2435,6 +2491,9 @@ const applyModelViewerSettings = (settings, { applyCamera = true, applyDesign = toggleEdges.checked = viewerState.edges; suppressEdgeReload = false; } + if (typeof viewerState.clayColor === "string") { + setClayColor(viewerState.clayColor, { apply: false }); + } if (typeof viewerState.displayMode === "string") { activeDisplayMode = viewerState.displayMode; applyDisplayMode(); @@ -2522,6 +2581,9 @@ const applyViewerState = (state) => { if (typeof state.addMode === "boolean" && addMode) { addMode.checked = state.addMode; } + if (typeof state.clayColor === "string") { + setClayColor(state.clayColor, { apply: false }); + } if (typeof state.displayMode === "string") { activeDisplayMode = state.displayMode; applyDisplayMode(); @@ -3115,6 +3177,9 @@ const initViewer = async () => { activeDisplayMode = params.get("displayMode") || "source"; setDisplayModeControlState(); } + if (params.has("clayColor")) { + setClayColor(params.get("clayColor"), { apply: activeDisplayMode === "clay" }); + } if (params.has("lightingMode") || params.has("light")) { activeLightingMode = params.get("lightingMode") || params.get("light") || "ambient"; applyLightingMode(); @@ -3133,6 +3198,7 @@ const initViewer = async () => { viewerState: { ...modelSettings.viewerState, displayMode: activeDisplayMode, + clayColor: activeClayColor, lightingMode: activeLightingMode, navigationAxis: activeNavigationAxis, zoomSpeed: activeZoomSpeed, @@ -3455,6 +3521,28 @@ const loadProjectSnapshot = async (project) => { }); setDisplayModeControlState(); } + if (clayColorControl && clayColorInput && clayColorHexInput) { + syncClayColorInputs(); + const applyClayInputColor = (value) => { + setClayColor(value, { apply: true }); + }; + clayColorInput.addEventListener("input", (event) => { + applyClayInputColor(event.target.value); + }); + clayColorInput.addEventListener("change", (event) => { + applyClayInputColor(event.target.value); + }); + clayColorHexInput.addEventListener("change", (event) => { + applyClayInputColor(event.target.value); + }); + clayColorHexInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + applyClayInputColor(event.target.value); + clayColorHexInput.blur(); + } + }); + updateClayColorControlVisibility(); + } if (lightingModeControl && lightingModeSelectHost) { lightingModeSelect = createGlassSelect({ host: lightingModeSelectHost, diff --git a/frontend/index.html b/frontend/index.html index 38ca7d8..0acbfd3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -87,7 +87,7 @@ - +