The 3D gamut viewer in Stage 5 (Verify Profile) has three significant usability problems that undermine its value as a diagnostic tool:
1. Unlabelled, misleading axes
The viewer uses a generic THREE.AxesHelper(100) which draws three coloured lines (Red, Green, Blue) starting at the origin. There are no text labels (L*, a*, b*) and the RGB line colours misleadingly suggest an RGB coordinate space rather than CIELAB. Without labels or tick marks, a user cannot interpret scale or orientation.
2. Messy, unlabelled sRGB reference wireframe
The sRGB reference gamut is rendered using MeshLambertMaterial({ wireframe: true }) on the raw triangulated mesh. This draws every internal triangle edge of the 892-face mesh, creating a dense, cluttered cage. It has no legend and no way to toggle it on/off. Users cannot tell what it represents.
3. No interactive controls or legend
There is no UI overlay to identify the two shapes (profile vs. reference), no toggle to isolate layers, and no ability to switch rendering modes. The profile gamut itself is rendered as a flat uniform blue (0x3b82f6) rather than per-vertex true colour.
Labelled bounding volume: Graduated axes with tick marks spanning the CIELAB domain (L*: 0→100, a*/b*: ±128).
Distinct rendering: Reference gamut as a clean structural wireframe (only hard edges, not internal triangulation) or faint transparent solid. Profile gamut as a solid with per-vertex true-colour shading.
Interactive controls: Toggle switches for visibility of each layer, rendering mode, and opacity.
Proposed Solution (6 Components)
Component 1: Custom CIELAB Axis Scaffold with CSS2D Labels
Remove the generic THREE.AxesHelper and THREE.GridHelper.
Draw three axis lines as THREE.Line with correct CIELAB ranges (L*: 0→100, a*/b*: −128→+128).
Add tick marks at key intervals (L* at 0/25/50/75/100, a*/b* at −128/−64/0/+64/+128).
Add a subtle bounding wire box (EdgesGeometry(BoxGeometry)) framing the full CIELAB domain.
Add a correctly scaled ground plane grid at L*=0.
Use CSS2DRenderer (new ~5KB addon) for crisp text labels: "L*", "a*", "b*", and colour-science indicators ("+a* (Red)", "−a* (Green)", "+b* (Yellow)", "−b* (Blue)").
Component 2: Clean sRGB Reference Gamut
Replace the wireframe render with THREE.EdgesGeometry(geometry, 15) rendered via LineSegments — this strips out all co-planar internal edges, leaving only the structural outline.
Add a faint transparent solid fill underneath (opacity ~0.08, depthWrite: false) to give the reference subtle physical presence.
Both objects in a THREE.Group for easy toggle control.
Component 3: Per-Vertex True-Colour Profile Gamut
Replace the flat blue MeshLambertMaterial({ color: 0x3b82f6 }) with per-vertex colouring.
For each vertex, compute labToSrgb(L, a, b) using the existing color_convert.js utility and store in a vertex colour buffer.
Use MeshLambertMaterial({ vertexColors: true, opacity: 0.85 }).
Result: the profile gamut blob shows its actual colours — reds are red, blues are blue, etc.
## Problem
The 3D gamut viewer in Stage 5 (Verify Profile) has three significant usability problems that undermine its value as a diagnostic tool:
### 1. Unlabelled, misleading axes
The viewer uses a generic `THREE.AxesHelper(100)` which draws three coloured lines (Red, Green, Blue) starting at the origin. There are no text labels (L*, a*, b*) and the RGB line colours misleadingly suggest an RGB coordinate space rather than CIELAB. Without labels or tick marks, a user cannot interpret scale or orientation.
### 2. Messy, unlabelled sRGB reference wireframe
The sRGB reference gamut is rendered using `MeshLambertMaterial({ wireframe: true })` on the raw triangulated mesh. This draws **every internal triangle edge** of the 892-face mesh, creating a dense, cluttered cage. It has no legend and no way to toggle it on/off. Users cannot tell what it represents.
### 3. No interactive controls or legend
There is no UI overlay to identify the two shapes (profile vs. reference), no toggle to isolate layers, and no ability to switch rendering modes. The profile gamut itself is rendered as a flat uniform blue (`0x3b82f6`) rather than per-vertex true colour.
---
## Screenshot (Current State)
[Screenshot 2026-09-02 090657.png](https://git.i3omb.com/attachments/ebabeae4-f799-4477-af3d-ab37ac013c80)
---
## Industry Context
Professional gamut visualisation tools (macOS ColorSync Utility, GamutVision, ICC Examin, Chromix ColorThink) follow consistent patterns:
- **Labelled bounding volume:** Graduated axes with tick marks spanning the CIELAB domain (L*: 0→100, a*/b*: ±128).
- **Distinct rendering:** Reference gamut as a clean structural wireframe (only hard edges, not internal triangulation) or faint transparent solid. Profile gamut as a solid with per-vertex true-colour shading.
- **Interactive controls:** Toggle switches for visibility of each layer, rendering mode, and opacity.
---
## Proposed Solution (6 Components)
### Component 1: Custom CIELAB Axis Scaffold with CSS2D Labels
- Remove the generic `THREE.AxesHelper` and `THREE.GridHelper`.
- Draw three axis lines as `THREE.Line` with correct CIELAB ranges (L*: 0→100, a*/b*: −128→+128).
- Add tick marks at key intervals (L* at 0/25/50/75/100, a*/b* at −128/−64/0/+64/+128).
- Add a subtle bounding wire box (`EdgesGeometry(BoxGeometry)`) framing the full CIELAB domain.
- Add a correctly scaled ground plane grid at L*=0.
- Use `CSS2DRenderer` (new ~5KB addon) for crisp text labels: "L*", "a*", "b*", and colour-science indicators ("+a* (Red)", "−a* (Green)", "+b* (Yellow)", "−b* (Blue)").
### Component 2: Clean sRGB Reference Gamut
- Replace the wireframe render with `THREE.EdgesGeometry(geometry, 15)` rendered via `LineSegments` — this strips out all co-planar internal edges, leaving only the structural outline.
- Add a faint transparent solid fill underneath (opacity ~0.08, `depthWrite: false`) to give the reference subtle physical presence.
- Both objects in a `THREE.Group` for easy toggle control.
### Component 3: Per-Vertex True-Colour Profile Gamut
- Replace the flat blue `MeshLambertMaterial({ color: 0x3b82f6 })` with per-vertex colouring.
- For each vertex, compute `labToSrgb(L, a, b)` using the existing `color_convert.js` utility and store in a vertex colour buffer.
- Use `MeshLambertMaterial({ vertexColors: true, opacity: 0.85 })`.
- Result: the profile gamut blob shows its actual colours — reds are red, blues are blue, etc.
### Component 4: Interactive Legend & Controls Overlay
- Add a glassmorphic floating panel (`position: absolute`) over the top-right corner of the viewer.
- Three rows: "Profile Gamut" / "sRGB Reference" / "CIELAB Axes", each with a colour swatch and a toggle switch.
- Toggles control `group.visible` for each layer.
### Component 5: CSS Styling
- Add dedicated gamut viewer styles: panel backdrop-filter, toggle switches matching the app's dark theme, CSS2D label typography.
### Component 6: Module Wiring
- Export toggle functions from `gamut_viewer.js`.
- Wire checkboxes to toggle functions.
- Add `CSS2DRenderer.js` script tag to `index.html`.
- Add `labelRenderer.render()` to the animation loop.
---
## Files Affected
| File | Action | Description |
|:-----|:-------|:------------|
| `src/js/gamut_viewer.js` | MODIFY | Major rework: axis scaffold, CSS2D labels, EdgesGeometry sRGB reference, per-vertex colour profile, toggle exports |
| `src/index.html` | MODIFY | Add CSS2DRenderer script tag; add legend/controls overlay HTML in Stage 5 |
| `src/styles/main.css` | MODIFY | Add gamut viewer section, controls panel, toggle switch, and CSS2D label styles |
| `src/js/CSS2DRenderer.js` | NEW | Three.js CSS2DRenderer addon (~5KB vendor file) |
After reviewing the codebase and the current gamut viewer implementation (src/js/gamut_viewer.js), here are my findings:
Missing Axes Labels: The current implementation uses a generic Three.js AxesHelper which draws Red, Green, and Blue lines mapped to the a*, L*, and b* axes, respectively. There are no textual labels (like L*, a*, b*) which makes it non-intuitive to read.
Unlabeled sRGB Wireframe: The wireframe in the background is indeed the sRGB reference gamut. It is loaded automatically on startup but lacks any UI legend or toggle to identify it or turn it off.
"Messy" Wireframe Appearance: The wireframe is currently drawn using raw triangulated mesh lines. This means every internal polygon edge is drawn, resulting in a cluttered and messy look, rather than a clean hull.
Better Approaches & External Ideas
Looking at industry standards like macOS ColorSync Utility and GamutVision:
Clean Axes: They typically provide a bounding box or grid with tick marks for the CIELAB space (-128 to 128 for a/b, 0 to 100 for L) rather than generic RGB infinite lines.
Comparison Visualization: When comparing gamuts, it is common to have one as an opaque solid and the other as a highly transparent solid (or point cloud), as overlapping wireframes become visually indecipherable.
Legends & Controls: Clear legends are always present to identify the reference vs. the subject, often with toggles to isolate views.
Proposed Options
1. Axes Presentation:
Option A: Add floating 3D text labels (L*, a*, b*) to the existing lines.
Option B (Recommended): Implement a bounding CIELAB grid box with clear tick marks, mimicking a true 3D graph appearance.
2. Reference Gamut (sRGB) Appearance:
Option A (Recommended): Change the reference from a wireframe to a faint, semi-transparent solid color.
Option B: Use THREE.EdgesGeometry to clean up the wireframe by removing coplanar inner triangles, leaving only the distinct outer hull edges.
3. UI Improvements (Highly Recommended):
Add a legend in the UI indicating what each shape/color represents.
Add a toggle control to show or hide the reference sRGB gamut.
I will also create an implementation plan in the current workspace based on these options for review.
### Findings Investigation
After reviewing the codebase and the current gamut viewer implementation (`src/js/gamut_viewer.js`), here are my findings:
1. **Missing Axes Labels:** The current implementation uses a generic Three.js `AxesHelper` which draws Red, Green, and Blue lines mapped to the `a*`, `L*`, and `b*` axes, respectively. There are no textual labels (like L*, a*, b*) which makes it non-intuitive to read.
2. **Unlabeled sRGB Wireframe:** The wireframe in the background is indeed the sRGB reference gamut. It is loaded automatically on startup but lacks any UI legend or toggle to identify it or turn it off.
3. **"Messy" Wireframe Appearance:** The wireframe is currently drawn using raw triangulated mesh lines. This means every internal polygon edge is drawn, resulting in a cluttered and messy look, rather than a clean hull.
### Better Approaches & External Ideas
Looking at industry standards like macOS ColorSync Utility and GamutVision:
- **Clean Axes:** They typically provide a bounding box or grid with tick marks for the CIELAB space (-128 to 128 for a/b, 0 to 100 for L) rather than generic RGB infinite lines.
- **Comparison Visualization:** When comparing gamuts, it is common to have one as an opaque solid and the other as a highly transparent solid (or point cloud), as overlapping wireframes become visually indecipherable.
- **Legends & Controls:** Clear legends are always present to identify the reference vs. the subject, often with toggles to isolate views.
### Proposed Options
**1. Axes Presentation:**
- **Option A:** Add floating 3D text labels (L*, a*, b*) to the existing lines.
- **Option B (Recommended):** Implement a bounding CIELAB grid box with clear tick marks, mimicking a true 3D graph appearance.
**2. Reference Gamut (sRGB) Appearance:**
- **Option A (Recommended):** Change the reference from a wireframe to a faint, semi-transparent solid color.
- **Option B:** Use `THREE.EdgesGeometry` to clean up the wireframe by removing coplanar inner triangles, leaving only the distinct outer hull edges.
**3. UI Improvements (Highly Recommended):**
- Add a legend in the UI indicating what each shape/color represents.
- Add a toggle control to show or hide the reference sRGB gamut.
I will also create an implementation plan in the current workspace based on these options for review.
gronod
changed title from Enhancement : 3d Gamut Visualisation is not ideal to Enhancement : 3D Gamut Visualisation Rework — Axes, Reference Rendering, Per-Vertex Colour & Controls2026-09-02 09:37:09 +01:00
Updated Analysis & Implementation Plan (Supersedes Previous Comment)
After a thorough code review and external research, the ticket body has been updated with a comprehensive 6-component implementation plan.
Key technical findings from the code review:
Axis system (gamut_viewer.js L42–49): Uses THREE.AxesHelper(100) which draws generic R/G/B lines with no labels, and a GridHelper(200, 20) that doesn't match the CIELAB coordinate range. The coordinate mapping is X=a*, Y=L*, Z=b*.
sRGB reference (gamut_viewer.js L202–212): loadSrgbReferenceGamut() calls renderGamutFromText() with isWireframe=true, which uses MeshLambertMaterial({ wireframe: true, opacity: 0.35 }). The sRGB.gam file contains 448 vertices and 892 triangular faces — all 892×3 = 2676 internal triangle edges are drawn, producing the cluttered appearance.
Profile gamut (gamut_viewer.js L214–224, called from colprof.js L195 and profcheck.js L101): loadGamutMesh() renders with color: 0x3b82f6, isWireframe: false — a flat opaque blue. The existing labToSrgb() function in color_convert.js is never used here, despite being the exact utility needed for per-vertex colouring.
No CSS exists for the gamut viewer section. The #gamutViewerContainer and .gamut-viewer-section have no dedicated styles in main.css.
No CSS2DRenderer is currently bundled. The project uses global <script> loading for Three.js (via three.min.js) and OrbitControls.js (IIFE pattern), so the CSS2DRenderer addon should follow the same pattern.
Recommended approach uses THREE.EdgesGeometry (threshold angle 15°) for a clean reference outline, per-vertex true-colour via the existing labToSrgb(), and CSS2DRenderer for crisp axis labels. Full detail in the updated ticket body above.
### Updated Analysis & Implementation Plan (Supersedes Previous Comment)
After a thorough code review and external research, the ticket body has been updated with a comprehensive 6-component implementation plan.
**Key technical findings from the code review:**
1. **Axis system** (`gamut_viewer.js` L42–49): Uses `THREE.AxesHelper(100)` which draws generic R/G/B lines with no labels, and a `GridHelper(200, 20)` that doesn't match the CIELAB coordinate range. The coordinate mapping is X=a*, Y=L*, Z=b*.
2. **sRGB reference** (`gamut_viewer.js` L202–212): `loadSrgbReferenceGamut()` calls `renderGamutFromText()` with `isWireframe=true`, which uses `MeshLambertMaterial({ wireframe: true, opacity: 0.35 })`. The sRGB.gam file contains 448 vertices and 892 triangular faces — all 892×3 = 2676 internal triangle edges are drawn, producing the cluttered appearance.
3. **Profile gamut** (`gamut_viewer.js` L214–224, called from `colprof.js` L195 and `profcheck.js` L101): `loadGamutMesh()` renders with `color: 0x3b82f6, isWireframe: false` — a flat opaque blue. The existing `labToSrgb()` function in `color_convert.js` is never used here, despite being the exact utility needed for per-vertex colouring.
4. **No CSS exists** for the gamut viewer section. The `#gamutViewerContainer` and `.gamut-viewer-section` have no dedicated styles in `main.css`.
5. **No CSS2DRenderer** is currently bundled. The project uses global `<script>` loading for Three.js (via `three.min.js`) and `OrbitControls.js` (IIFE pattern), so the CSS2DRenderer addon should follow the same pattern.
**Recommended approach** uses `THREE.EdgesGeometry` (threshold angle 15°) for a clean reference outline, per-vertex true-colour via the existing `labToSrgb()`, and `CSS2DRenderer` for crisp axis labels. Full detail in the updated ticket body above.
Axes now draw L*, a*, b* lines with correct CIELAB ranges and tick marks at 25/50/75/100 and ±64/±128. Crisp CSS2D text labels with colour-coded indicators (+a* red, −a* green, +b* yellow, −b* blue)
sRGB reference uses EdgesGeometry(geometry, 15°) — only structural hull edges are drawn, no internal triangle edges. A very faint solid fill (opacity 0.07) gives it physical presence
Profile gamut is per-vertex coloured via labToSrgb() — the blob naturally shows reds, greens, yellows and blues in the right regions
Glassmorphic legend panel (top-right overlay) with three iOS-style toggle switches to independently show/hide Profile Gamut, sRGB Reference, and CIELAB Axes
Orbit camera target adjusted to L*=50 (centre of CIELAB space) for better default orientation
Implemented in `enh/185-gamut-viewer` (stacked on `enh/176-colprof-fwa`):
- Added `resetCamera()` public API and a Reset View button.
- Added per-layer opacity sliders for the profile gamut, sRGB reference, and CIELAB axes.
- Added `R` keyboard shortcut to reset the camera while Stage 5 is active.
- Added JSDoc to all public `gamut_viewer.js` functions.
- Added supporting CSS for the new controls and updated documentation.
Closes #185.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The 3D gamut viewer in Stage 5 (Verify Profile) has three significant usability problems that undermine its value as a diagnostic tool:
1. Unlabelled, misleading axes
The viewer uses a generic
THREE.AxesHelper(100)which draws three coloured lines (Red, Green, Blue) starting at the origin. There are no text labels (L*, a*, b*) and the RGB line colours misleadingly suggest an RGB coordinate space rather than CIELAB. Without labels or tick marks, a user cannot interpret scale or orientation.2. Messy, unlabelled sRGB reference wireframe
The sRGB reference gamut is rendered using
MeshLambertMaterial({ wireframe: true })on the raw triangulated mesh. This draws every internal triangle edge of the 892-face mesh, creating a dense, cluttered cage. It has no legend and no way to toggle it on/off. Users cannot tell what it represents.3. No interactive controls or legend
There is no UI overlay to identify the two shapes (profile vs. reference), no toggle to isolate layers, and no ability to switch rendering modes. The profile gamut itself is rendered as a flat uniform blue (
0x3b82f6) rather than per-vertex true colour.Screenshot (Current State)
Screenshot 2026-09-02 090657.png
Industry Context
Professional gamut visualisation tools (macOS ColorSync Utility, GamutVision, ICC Examin, Chromix ColorThink) follow consistent patterns:
Proposed Solution (6 Components)
Component 1: Custom CIELAB Axis Scaffold with CSS2D Labels
THREE.AxesHelperandTHREE.GridHelper.THREE.Linewith correct CIELAB ranges (L*: 0→100, a*/b*: −128→+128).EdgesGeometry(BoxGeometry)) framing the full CIELAB domain.CSS2DRenderer(new ~5KB addon) for crisp text labels: "L*", "a*", "b*", and colour-science indicators ("+a* (Red)", "−a* (Green)", "+b* (Yellow)", "−b* (Blue)").Component 2: Clean sRGB Reference Gamut
THREE.EdgesGeometry(geometry, 15)rendered viaLineSegments— this strips out all co-planar internal edges, leaving only the structural outline.depthWrite: false) to give the reference subtle physical presence.THREE.Groupfor easy toggle control.Component 3: Per-Vertex True-Colour Profile Gamut
MeshLambertMaterial({ color: 0x3b82f6 })with per-vertex colouring.labToSrgb(L, a, b)using the existingcolor_convert.jsutility and store in a vertex colour buffer.MeshLambertMaterial({ vertexColors: true, opacity: 0.85 }).Component 4: Interactive Legend & Controls Overlay
position: absolute) over the top-right corner of the viewer.group.visiblefor each layer.Component 5: CSS Styling
Component 6: Module Wiring
gamut_viewer.js.CSS2DRenderer.jsscript tag toindex.html.labelRenderer.render()to the animation loop.Files Affected
src/js/gamut_viewer.jssrc/index.htmlsrc/styles/main.csssrc/js/CSS2DRenderer.jsFindings Investigation
After reviewing the codebase and the current gamut viewer implementation (
src/js/gamut_viewer.js), here are my findings:AxesHelperwhich draws Red, Green, and Blue lines mapped to thea*,L*, andb*axes, respectively. There are no textual labels (like L*, a*, b*) which makes it non-intuitive to read.Better Approaches & External Ideas
Looking at industry standards like macOS ColorSync Utility and GamutVision:
Proposed Options
1. Axes Presentation:
2. Reference Gamut (sRGB) Appearance:
THREE.EdgesGeometryto clean up the wireframe by removing coplanar inner triangles, leaving only the distinct outer hull edges.3. UI Improvements (Highly Recommended):
I will also create an implementation plan in the current workspace based on these options for review.
Enhancement : 3d Gamut Visualisation is not idealto Enhancement : 3D Gamut Visualisation Rework — Axes, Reference Rendering, Per-Vertex Colour & ControlsUpdated Analysis & Implementation Plan (Supersedes Previous Comment)
After a thorough code review and external research, the ticket body has been updated with a comprehensive 6-component implementation plan.
Key technical findings from the code review:
Axis system (
gamut_viewer.jsL42–49): UsesTHREE.AxesHelper(100)which draws generic R/G/B lines with no labels, and aGridHelper(200, 20)that doesn't match the CIELAB coordinate range. The coordinate mapping is X=a*, Y=L*, Z=b*.sRGB reference (
gamut_viewer.jsL202–212):loadSrgbReferenceGamut()callsrenderGamutFromText()withisWireframe=true, which usesMeshLambertMaterial({ wireframe: true, opacity: 0.35 }). The sRGB.gam file contains 448 vertices and 892 triangular faces — all 892×3 = 2676 internal triangle edges are drawn, producing the cluttered appearance.Profile gamut (
gamut_viewer.jsL214–224, called fromcolprof.jsL195 andprofcheck.jsL101):loadGamutMesh()renders withcolor: 0x3b82f6, isWireframe: false— a flat opaque blue. The existinglabToSrgb()function incolor_convert.jsis never used here, despite being the exact utility needed for per-vertex colouring.No CSS exists for the gamut viewer section. The
#gamutViewerContainerand.gamut-viewer-sectionhave no dedicated styles inmain.css.No CSS2DRenderer is currently bundled. The project uses global
<script>loading for Three.js (viathree.min.js) andOrbitControls.js(IIFE pattern), so the CSS2DRenderer addon should follow the same pattern.Recommended approach uses
THREE.EdgesGeometry(threshold angle 15°) for a clean reference outline, per-vertex true-colour via the existinglabToSrgb(), andCSS2DRendererfor crisp axis labels. Full detail in the updated ticket body above.Implementation Complete
All 6 components from the implementation plan have been delivered.
Files changed:
src/js/CSS2DRenderer.jsTHREE.CSS2DObjectandTHREE.CSS2DRendereras globalssrc/js/gamut_viewer.jssrc/index.htmlCSS2DRenderer.jsscript tag; replaced minimal gamut section with header, viewer wrap, and glassmorphic legend overlay panelsrc/styles/main.cssWhat has changed visually:
EdgesGeometry(geometry, 15°)— only structural hull edges are drawn, no internal triangle edges. A very faint solid fill (opacity 0.07) gives it physical presencelabToSrgb()— the blob naturally shows reds, greens, yellows and blues in the right regionsBuild status: Rust compiled successfully. JS syntax checks pass.
Implemented in
enh/185-gamut-viewer(stacked onenh/176-colprof-fwa):resetCamera()public API and a Reset View button.Rkeyboard shortcut to reset the camera while Stage 5 is active.gamut_viewer.jsfunctions.Closes #185.