From 1058381a9bbb2caace7ea6ce87d9ffc9980c363a Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Thu, 26 Feb 2026 10:18:38 +0000 Subject: [PATCH 1/2] Implement automatic color picker paint mode transitions --- ui/colourpicker.css | 5 ++ ui/colourpicker.js | 42 +++++++++++--- ui/gizmos.js | 131 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 143 insertions(+), 35 deletions(-) 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..e3aa5f58 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 pickerPointerType = "mouse"; +let pickerContentElement = null; +let colorButtonLastPointerType = "mouse"; + + 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 }); + detachPickerHoverHandlers(); }, target: document.body, }); @@ -103,46 +116,108 @@ 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) { + pickerPointerType = colorButtonLastPointerType || detectPickerPointerType(); + paintModeExplicit = false; colorPicker.open(window.selectedColor); + bindPickerHoverHandlers(); + 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); +} + +function activatePaintMode() { + if (paintModeActive) { + updatePaintToolVisualState(); + return; + } + + const canvas = flock.scene.getEngine().getRenderingCanvas(); + if (!canvas) return; - startColorPickingKeyboardMode(onPickMesh); + 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 bindPickerHoverHandlers() { + detachPickerHoverHandlers(); + if (!colorPicker?.container) return; + + pickerContentElement = colorPicker.container.querySelector(".color-picker-content"); + if (!pickerContentElement || pickerPointerType !== "mouse") return; + + pickerContentElement.addEventListener("pointerleave", handlePickerPointerLeave); + pickerContentElement.addEventListener("pointerenter", handlePickerPointerEnter); +} + +function detachPickerHoverHandlers() { + if (!pickerContentElement) return; + pickerContentElement.removeEventListener("pointerleave", handlePickerPointerLeave); + pickerContentElement.removeEventListener("pointerenter", handlePickerPointerEnter); + pickerContentElement = null; +} + +function handlePickerPointerLeave(event) { + if (!colorPicker?.isOpen) return; + if (event.pointerType && event.pointerType !== "mouse") return; + activatePaintMode(); +} + +function handlePickerPointerEnter(event) { + if (!colorPicker?.isOpen) return; + if (event.pointerType && event.pointerType !== "mouse") return; + deactivatePaintMode(); +} + +function updatePaintToolVisualState() { + colorPicker?.updatePaintModeButtonVisual?.(paintModeExplicit || paintModeActive); } function applyColorAtPosition(canvasX, canvasY) { @@ -160,10 +235,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); } } From 158bb23d7a974961b6696d182e56640c761a0aa3 Mon Sep 17 00:00:00 2001 From: Dr Tracy Gardner Date: Thu, 26 Feb 2026 10:43:35 +0000 Subject: [PATCH 2/2] Fix paint mode activation and toolbar highlight behavior --- style.css | 6 ++++- ui/gizmos.js | 68 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 22 deletions(-) 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/gizmos.js b/ui/gizmos.js index e3aa5f58..8f0315a4 100644 --- a/ui/gizmos.js +++ b/ui/gizmos.js @@ -39,9 +39,9 @@ let colorPickingCirclePosition = { x: 0, y: 0 }; let _onPickMeshRef = null; let paintModeActive = false; let paintModeExplicit = false; -let pickerPointerType = "mouse"; let pickerContentElement = null; let colorButtonLastPointerType = "mouse"; +let pickerPointerMoveBound = false; document.addEventListener("DOMContentLoaded", function () { @@ -106,7 +106,7 @@ document.addEventListener("DOMContentLoaded", function () { }, onClose: () => { deactivatePaintMode({ clearExplicit: true }); - detachPickerHoverHandlers(); + detachPickerPointerTracking(); }, target: document.body, }); @@ -123,10 +123,11 @@ document.addEventListener("DOMContentLoaded", function () { colorButton.addEventListener("click", (event) => { event.preventDefault(); if (colorPicker) { - pickerPointerType = colorButtonLastPointerType || detectPickerPointerType(); + colorButtonLastPointerType = + colorButtonLastPointerType || detectPickerPointerType(); paintModeExplicit = false; colorPicker.open(window.selectedColor); - bindPickerHoverHandlers(); + bindPickerPointerTracking(); updatePaintToolVisualState(); } }); @@ -186,38 +187,63 @@ function deactivatePaintMode({ clearExplicit = false } = {}) { updatePaintToolVisualState(); } -function bindPickerHoverHandlers() { - detachPickerHoverHandlers(); +function bindPickerPointerTracking() { + detachPickerPointerTracking(); if (!colorPicker?.container) return; pickerContentElement = colorPicker.container.querySelector(".color-picker-content"); - if (!pickerContentElement || pickerPointerType !== "mouse") return; + if (!pickerContentElement) return; - pickerContentElement.addEventListener("pointerleave", handlePickerPointerLeave); - pickerContentElement.addEventListener("pointerenter", handlePickerPointerEnter); + document.addEventListener("pointermove", handlePickerPointerMove, true); + pickerPointerMoveBound = true; } -function detachPickerHoverHandlers() { - if (!pickerContentElement) return; - pickerContentElement.removeEventListener("pointerleave", handlePickerPointerLeave); - pickerContentElement.removeEventListener("pointerenter", handlePickerPointerEnter); +function detachPickerPointerTracking() { + if (pickerPointerMoveBound) { + document.removeEventListener("pointermove", handlePickerPointerMove, true); + pickerPointerMoveBound = false; + } + pickerContentElement = null; } -function handlePickerPointerLeave(event) { +function handlePickerPointerMove(event) { if (!colorPicker?.isOpen) return; - if (event.pointerType && event.pointerType !== "mouse") return; - activatePaintMode(); -} -function handlePickerPointerEnter(event) { - if (!colorPicker?.isOpen) return; if (event.pointerType && event.pointerType !== "mouse") return; - deactivatePaintMode(); + + 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() { - colorPicker?.updatePaintModeButtonVisual?.(paintModeExplicit || paintModeActive); + 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) {