diff --git a/style.css b/style.css index 56c15233..995cdb3e 100644 --- a/style.css +++ b/style.css @@ -542,6 +542,11 @@ button { fill: #ffffff !important; } +#colorPickerButton.paint-mode-active { + border: 2px solid var(--paint-mode-color, #ffffff); + box-shadow: 0 0 0 2px rgba(81, 29, 145, 0.45); +} + .bigbutton svg { filter: none; /* light mode: normal */ } @@ -1229,4 +1234,3 @@ details summary:focus, list-style: none; } - diff --git a/ui/colourpicker.css b/ui/colourpicker.css index 0450475a..16b4c2df 100644 --- a/ui/colourpicker.css +++ b/ui/colourpicker.css @@ -288,6 +288,11 @@ background-color: var(--color-button-bg-hover); } +.color-picker-use.paint-mode-active { + border: 2px solid var(--paint-mode-color, #ffffff); + box-shadow: 0 0 0 2px rgba(81, 29, 145, 0.45); +} + .color-picker-cancel { background-color: var(--color-button-primary); color: white; diff --git a/ui/colourpicker.js b/ui/colourpicker.js index ecc07c90..d81de165 100644 --- a/ui/colourpicker.js +++ b/ui/colourpicker.js @@ -85,9 +85,11 @@ class CustomColorPicker { this.currentColor = options.color || "#ff0000"; this.onColorChange = options.onColorChange || (() => {}); this.onClose = options.onClose || (() => {}); + this.onPaintButtonClick = options.onPaintButtonClick || (() => {}); this.targetElement = options.target || document.body; this.isOpen = false; + this._isClosing = false; // Eyedropper state this._eyedropperActive = false; @@ -396,6 +398,7 @@ class CustomColorPicker { ".current-color-display", ); this.currentColorText = this.container.querySelector(".current-color-text"); + this.useButton = this.container.querySelector(".color-picker-use"); // Lightness slider refs this.lightSlider = this.container.querySelector(".lightness-slider"); @@ -763,9 +766,12 @@ class CustomColorPicker { }); // Confirm / general keyboard handling on the container (Esc/Enter/Space) - this.container - .querySelector(".color-picker-footer") - .addEventListener("click", () => this.confirmColor()); + if (this.useButton) { + this.useButton.addEventListener("click", (e) => { + e.preventDefault(); + this.onPaintButtonClick(this.currentColor); + }); + } this.container.addEventListener("keydown", (e) => this.handleKeydown(e)); if (this.lightSlider) { @@ -1042,7 +1048,7 @@ class CustomColorPicker { // Is the Use button (or inside it)? if (t.closest(".color-picker-use")) { e.preventDefault(); - this.confirmColor(); + this.onPaintButtonClick(this.currentColor); return; } @@ -1188,6 +1194,8 @@ class CustomColorPicker { colorDisplay.style.backgroundColor = this.currentColor; } + this.updatePaintModeButtonVisual(this._paintModeButtonActive === true); + // Sync inputs + sliders this.updateCssInput(); this.updateRgbInputs(); @@ -1963,17 +1971,37 @@ class CustomColorPicker { } close() { + if (this._isClosing) return; + this._isClosing = true; + this.container.style.display = "none"; this.isOpen = false; document.removeEventListener("click", this.outsideClickHandler, true); + + this.updatePaintModeButtonVisual(false); + + if (this.onClose) { + this.onClose(); + } + + this._isClosing = false; + } + + updatePaintModeButtonVisual(isActive) { + this._paintModeButtonActive = isActive; + if (!this.useButton) return; + + this.useButton.classList.toggle("paint-mode-active", isActive); + if (isActive) { + this.useButton.style.setProperty("--paint-mode-color", this.currentColor); + } else { + this.useButton.style.removeProperty("--paint-mode-color"); + } } confirmColor() { this.onColorChange(this.currentColor); this.close(); - if (this.onClose) { - setTimeout(() => this.onClose(), 100); - } } } diff --git a/ui/gizmos.js b/ui/gizmos.js index a05fff85..8f0315a4 100644 --- a/ui/gizmos.js +++ b/ui/gizmos.js @@ -36,6 +36,14 @@ let colorPickingCallback = null; let colorPickingCircle = null; let colorPickingCirclePosition = { x: 0, y: 0 }; +let _onPickMeshRef = null; +let paintModeActive = false; +let paintModeExplicit = false; +let pickerContentElement = null; +let colorButtonLastPointerType = "mouse"; +let pickerPointerMoveBound = false; + + document.addEventListener("DOMContentLoaded", function () { const colorButton = document.getElementById("colorPickerButton"); @@ -90,10 +98,15 @@ document.addEventListener("DOMContentLoaded", function () { color: window.selectedColor, onColorChange: (newColor) => { window.selectedColor = newColor; + updatePaintToolVisualState(); + }, + onPaintButtonClick: () => { + paintModeExplicit = true; + activatePaintMode(); }, onClose: () => { - // After color picker closes, start mesh selection - pickMeshFromCanvas(); + deactivatePaintMode({ clearExplicit: true }); + detachPickerPointerTracking(); }, target: document.body, }); @@ -103,46 +116,134 @@ document.addEventListener("DOMContentLoaded", function () { // Attach click event to open custom color picker if (colorButton) { + colorButton.addEventListener("pointerdown", (event) => { + colorButtonLastPointerType = event.pointerType || "mouse"; + }); + colorButton.addEventListener("click", (event) => { event.preventDefault(); if (colorPicker) { + colorButtonLastPointerType = + colorButtonLastPointerType || detectPickerPointerType(); + paintModeExplicit = false; colorPicker.open(window.selectedColor); + bindPickerPointerTracking(); + updatePaintToolVisualState(); } }); } }); -let _onPickMeshRef = null; +function detectPickerPointerType() { + return window.matchMedia?.("(pointer: coarse)").matches ? "touch" : "mouse"; +} + +function onPaintMeshClick(event) { + if (!paintModeActive) return; -function pickMeshFromCanvas() { const canvas = flock.scene.getEngine().getRenderingCanvas(); + if (!canvas) return; - const onPickMesh = function (event) { - const canvasRect = canvas.getBoundingClientRect(); - - // Exit if outside canvas - if (eventIsOutOfCanvasBounds(event, canvasRect)) { - window.removeEventListener("click", onPickMesh); - endColorPickingMode(); - // restore cursors - document.body.style.cursor = "default"; - canvas.style.cursor = "auto"; - return; - } + const canvasRect = canvas.getBoundingClientRect(); + if (eventIsOutOfCanvasBounds(event, canvasRect)) return; - const [canvasX, canvasY] = getCanvasXAndCanvasYValues(event, canvasRect); - applyColorAtPosition(canvasX, canvasY); - document.body.style.cursor = "crosshair"; - canvas.style.cursor = "crosshair"; - }; + const [canvasX, canvasY] = getCanvasXAndCanvasYValues(event, canvasRect); + applyColorAtPosition(canvasX, canvasY); +} - startColorPickingKeyboardMode(onPickMesh); +function activatePaintMode() { + if (paintModeActive) { + updatePaintToolVisualState(); + return; + } + + const canvas = flock.scene.getEngine().getRenderingCanvas(); + if (!canvas) return; + + paintModeActive = true; + startColorPickingKeyboardMode(onPaintMeshClick); + window.addEventListener("click", onPaintMeshClick); document.body.style.cursor = "crosshair"; canvas.style.cursor = "crosshair"; - setTimeout(() => { - window.addEventListener("click", onPickMesh); - }, 200); + updatePaintToolVisualState(); +} + +function deactivatePaintMode({ clearExplicit = false } = {}) { + if (!paintModeActive && !clearExplicit) return; + + paintModeActive = false; + if (clearExplicit) paintModeExplicit = false; + + window.removeEventListener("click", onPaintMeshClick); + endColorPickingMode(); + + const canvas = flock.scene.getEngine().getRenderingCanvas(); + document.body.style.cursor = "default"; + if (canvas) { + canvas.style.cursor = "auto"; + } + + updatePaintToolVisualState(); +} + +function bindPickerPointerTracking() { + detachPickerPointerTracking(); + if (!colorPicker?.container) return; + + pickerContentElement = colorPicker.container.querySelector(".color-picker-content"); + if (!pickerContentElement) return; + + document.addEventListener("pointermove", handlePickerPointerMove, true); + pickerPointerMoveBound = true; +} + +function detachPickerPointerTracking() { + if (pickerPointerMoveBound) { + document.removeEventListener("pointermove", handlePickerPointerMove, true); + pickerPointerMoveBound = false; + } + + pickerContentElement = null; +} + +function handlePickerPointerMove(event) { + if (!colorPicker?.isOpen) return; + + if (event.pointerType && event.pointerType !== "mouse") return; + + if (!pickerContentElement) return; + + const rect = pickerContentElement.getBoundingClientRect(); + const isInsidePicker = + event.clientX >= rect.left && + event.clientX <= rect.right && + event.clientY >= rect.top && + event.clientY <= rect.bottom; + + if (isInsidePicker) { + deactivatePaintMode(); + return; + } + + activatePaintMode(); +} + +function updatePaintToolVisualState() { + const colorPickerButton = document.getElementById("colorPickerButton"); + if (!colorPickerButton) return; + + const shouldShowActive = paintModeExplicit || paintModeActive; + colorPickerButton.classList.toggle("paint-mode-active", shouldShowActive); + + if (shouldShowActive) { + colorPickerButton.style.setProperty("--paint-mode-color", window.selectedColor); + } else { + colorPickerButton.style.removeProperty("--paint-mode-color"); + } + + // Keep the in-dialog paint button neutral; active indicator belongs on the canvas tool button. + colorPicker?.updatePaintModeButtonVisual?.(false); } function applyColorAtPosition(canvasX, canvasY) { @@ -160,10 +261,10 @@ function applyColorAtPosition(canvasX, canvasY) { const pickedMesh = pickLeafFromRay(pickRay, scene); if (pickedMesh) { - updateBlockColorAndHighlight(pickedMesh, selectedColor); + updateBlockColorAndHighlight(pickedMesh, window.selectedColor); } else { - flock.setSky(selectedColor); - updateBlockColorAndHighlight(meshMap?.["sky"], selectedColor); + flock.setSky(window.selectedColor); + updateBlockColorAndHighlight(meshMap?.["sky"], window.selectedColor); } }