Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions packages/pixel-draw-renderer/docs/llms/PROMPT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Task: Implement UV Mapping System in `pixel-draw-renderer`

## Overview

I want you to create a detailed implementation plan for a full UV mapping system
within the `pixel-draw-renderer` workspace. The goal is to allow users to define,
manage, and edit UV islands on a texture atlas, with real-time updates reflected
on Three.js geometries — similar to how faces are textured in the `voxel-renderer`
workspace (see `src/blocks/shapes/**.ts`).

---

## Context & Architecture

Before planning, please explore and understand the current codebase:

- **`pixel-draw-renderer`**: The main workspace where UV editing will live.
Understand its current rendering pipeline, canvas management, tools/modes system,
and how it handles user interaction.
- **`voxel-renderer`** (`src/blocks/shapes/**.ts`): Understand how geometry faces
are currently UV-mapped (hardcoded or otherwise), and how the texture is consumed
by Three.js materials. This is the downstream consumer we need to feed into.
- **Blockbench reference** (`uv.js`):
https://github.com/JannisX11/blockbench/blob/master/js/texturing/uv.js
Use this as inspiration for UV data modeling, face-UV assignment patterns,
multi-UV management, and interaction paradigms (not for direct copy-paste).

---

## Features to Plan & Implement

### 1. UV Data Model
- Define a `UV` class or interface: position (`u`, `v`), size (`width`, `height`),
rotation, and an associated face ID or label.
- Support multiple UVs per texture (UV islands / atlas regions).
- UVs should be serializable (JSON) so they can be saved/restored with the project.
- Consider a `UVMap` registry that holds all UVs for a given texture.

### 2. UV Editing Mode
- Introduce a new distinct **UV Edit Mode** (separate from draw/erase/select modes)
that can be toggled from the toolbar or via a keyboard shortcut.
- In UV Edit Mode, the canvas overlays UV regions as colored, semi-transparent
rectangles with handles.
- The user should be able to:
- **Add** a new UV island (click + drag to define bounds).
- **Select** an existing UV (click to highlight).
- **Move** a UV (drag selected UV across the texture surface).
- **Resize** a UV (drag corner/edge handles).
- **Delete** a UV (keyboard shortcut or context menu).
- **Label/rename** a UV (to match face IDs from voxel-renderer).

### 3. UV Stacking & Face Management
- Allow multiple UVs to be **stacked** (overlapping, sharing the same texture region),
modeling the concept of multiple geometry faces sharing the same UV space.
- Provide a **stack/unstack** toggle per UV island.
- Display a face panel (sidebar or floating panel) listing all defined UVs/faces,
inspired by how Blockbench lists cube faces. Each entry should show:
- Face label (e.g. `top`, `bottom`, `north`, `south`, `east`, `west`)
- UV coordinates
- A visibility toggle
- A "jump to UV" button

### 4. Real-Time Three.js Texture Sync
- When a UV is moved or resized, recompute the UV coordinates for the associated
face and update the Three.js `BufferGeometry` attribute (`uv`) in real time.
- Ensure the Three.js material texture is flagged `needsUpdate = true` after
pixel edits that affect a UV region.
- Explore whether this sync should be event-driven (emitting a `uv:changed` event)
or reactive (MobX/signals/store-based), consistent with the existing architecture.

### 5. Snapping & Grid Alignment
- UVs should snap to the pixel grid by default (configurable).
- Optional snap-to-other-UVs behavior (edge alignment).

### 6. Undo/Redo Support
- All UV operations (add, move, resize, delete) must integrate with the existing
undo/redo history stack.

### 7. Update the example
- Update the demo in `./examples` so we are able to test everything visually with Three.js

---

## Deliverables Expected in the Plan

1. **File & folder structure** — where new UV-related classes, stores, and components
should live within the existing project layout.
2. **Class/interface definitions** — outline the key data structures (`UV`, `UVMap`,
`UVEditTool`, etc.) with their properties and methods.
3. **Mode system integration** — how to hook UV Edit Mode into the existing
tool/mode architecture without breaking current modes.
4. **Rendering pipeline** — how UV overlays are drawn on the canvas (separate
overlay canvas, SVG layer, or direct canvas compositing?).
5. **Three.js sync strategy** — the exact mechanism and touch points for keeping
geometry UVs in sync with edits.
6. **Migration path** — how to convert any existing hardcoded UVs in `voxel-renderer`
shapes into the new system without breaking current behavior.
7. **Open questions** — flag any architectural ambiguities that need a decision
before implementation begins.

---

## Constraints & Preferences

- Stay consistent with the existing code style, patterns, and abstractions already
present in the workspace.
- Prefer incremental, non-breaking changes — the voxel-renderer should keep working
throughout the migration.
- Performance matters: UV overlay rendering should not degrade canvas frame rate
during pixel drawing.
23 changes: 16 additions & 7 deletions packages/pixel-draw-renderer/examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,28 @@
<div class="toolbar-item">
<button class="mode-btn" id="mode-paint">Paint</button>
<button class="mode-btn" id="mode-move">Move</button>
<button class="mode-btn" id="mode-uv">UV</button>
</div>
<div class="toolbar-item">
<button id="color-swatch" title="Color"></button>
<div id="paint-controls" class="toolbar-item-group">
<div class="toolbar-item">
<button id="color-swatch" title="Color"></button>
</div>
<label class="toolbar-item">
Size
<input type="range" id="brush-size" min="1" max="32" value="1">
<span id="brush-size-display">1px</span>
</label>
</div>
<label class="toolbar-item">
Size
<input type="range" id="brush-size" min="1" max="32" value="1">
<span id="brush-size-display">1px</span>
</label>
</div>
</div>
<div id="canvas-container">
<canvas tabindex="-1" id="three-canvas"></canvas>
<aside id="uv-panel">
<div class="uv-panel-header">UV Regions</div>
<div id="uv-face-list"></div>
<div id="uv-coords"></div>
<p class="uv-hint">Drag to reposition · Del to delete</p>
</aside>
</div>
<script type="module" src="./scripts/main.ts"></script>
</body>
Expand Down
106 changes: 106 additions & 0 deletions packages/pixel-draw-renderer/examples/public/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ html.handle-dragging.vertical * {
user-select: none;
}

.toolbar-item-group {
display: flex;
align-items: center;
gap: 6px;
}

.toolbar-item {
display: flex;
align-items: center;
Expand Down Expand Up @@ -123,3 +129,103 @@ html.handle-dragging.vertical * {
font-size: 11px;
color: #ccc;
}

/* ─── UV Panel ──────────────────────────────────────────────────────────── */

#uv-panel {
display: none;
flex-direction: column;
position: absolute;
top: 8px;
right: 8px;
width: 160px;
background: rgba(20, 28, 36, 0.88);
border: 1px solid #334;
border-radius: 6px;
overflow: hidden;
font-family: sans-serif;
font-size: 12px;
color: #ccc;
z-index: 10;
backdrop-filter: blur(4px);
}

#uv-panel.visible {
display: flex;
}

.uv-panel-header {
padding: 6px 10px;
font-size: 11px;
font-weight: 600;
color: #aab;
background: rgba(0, 0, 0, 0.3);
letter-spacing: 0.04em;
text-transform: uppercase;
border-bottom: 1px solid #334;
}

#uv-face-list {
display: flex;
flex-direction: column;
overflow-y: auto;
max-height: 220px;
}

.uv-face-item {
display: flex;
align-items: center;
gap: 7px;
padding: 5px 10px;
cursor: pointer;
transition: background 0.1s;
}

.uv-face-item:hover {
background: rgba(255, 255, 255, 0.07);
}

.uv-face-item.active {
background: rgba(68, 136, 255, 0.2);
color: #fff;
}

.uv-face-color {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 2px;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.15);
}

.uv-face-name {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

#uv-coords {
display: none;
grid-template-columns: auto 1fr;
gap: 3px 8px;
padding: 6px 10px;
border-top: 1px solid #334;
font-size: 11px;
color: #aab;
}

.uv-coord-label {
color: #668;
font-weight: 600;
}

.uv-hint {
margin: 0;
padding: 6px 10px;
font-size: 10px;
color: #556;
border-top: 1px solid #334;
text-align: center;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class CameraBehavior extends CameraComponent {
far: 100
});

this.threeCamera.position.set(0, 0, 8);
this.threeCamera.position.set(0, 0, 10);
}

get camera(): THREE.PerspectiveCamera {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Import Third-party Dependencies
import {
type Actor,
ActorComponent
} from "@jolly-pixel/engine";
import * as THREE from "three";

export interface CubeWithUVOptions {
canvasTexture: THREE.CanvasTexture;
/** World-space position for this cube. */
position: { x: number; y: number; z: number; };
/** Rotation speed multiplier (default 1). */
speed?: number;
/** Which axes to rotate on. */
rotateAxes?: { x?: boolean; y?: boolean; z?: boolean; };
}

/**
* A rotating cube whose UV attributes are updated externally.
* Exposes `geometry` so the caller can rebuild UVs on demand.
*/
export class CubeWithUV extends ActorComponent {
readonly geometry: THREE.BoxGeometry;
readonly mesh: THREE.Mesh;

#speed: number;
#axes: { x: boolean; y: boolean; z: boolean; };
#canvasTexture: THREE.CanvasTexture;

constructor(
actor: Actor,
options: CubeWithUVOptions
) {
super({ actor, typeName: "CubeWithUV" });

this.#canvasTexture = options.canvasTexture;
this.#speed = options.speed ?? 1;
this.#axes = {
x: options.rotateAxes?.x ?? false,
y: options.rotateAxes?.y ?? true,
z: options.rotateAxes?.z ?? false
};

this.geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);

this.mesh = new THREE.Mesh(
this.geometry,
new THREE.MeshStandardMaterial({
map: this.#canvasTexture,
transparent: true
})
);

this.mesh.position.set(options.position.x, options.position.y, options.position.z);

this.actor.addChildren(this.mesh);
}

update(): void {
const dt = 0.008 * this.#speed;
if (this.#axes.x) {
this.mesh.rotation.x += dt * 0.7;
}
if (this.#axes.y) {
this.mesh.rotation.y += dt;
}
if (this.#axes.z) {
this.mesh.rotation.z += dt * 0.5;
}
this.#canvasTexture.needsUpdate = true;
}
}
Loading
Loading