Skip to content

Commit 568efa6

Browse files
author
DavidQ
committed
Stabilize Vector Asset Studio controls and selection under unified UX contract - PR 10.11
1 parent bcede6d commit 568efa6

8 files changed

Lines changed: 170 additions & 13 deletions

File tree

docs/dev/codex_commands.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
model: gpt-5.3-codex
22
reasoning: medium
33

4-
Apply PR_10_10_TILEMAP_STUDIO_UAT
4+
Apply PR_10_11_VECTOR_ASSET_STUDIO_UAT
55

6-
- Enforce empty state messaging
7-
- Add first-tile auto-selection
8-
- Add selection highlight
9-
- Enforce control enable/disable
10-
- Stabilize canvas rendering
6+
- Add first-element auto-selection
7+
- Fix palette/paint/stroke enablement
8+
- Enforce control enable/disable rules
9+
- Ensure selection highlight
1110
- Ensure workspace stability
1211
- Do not modify data layer

docs/dev/commit_comment.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Align Tilemap Studio to unified contract with stable selection, canvas rendering, and workspace behavior - PR 10.10
1+
Stabilize Vector Asset Studio controls and selection under unified UX contract - PR 10.11
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# PR 10.11 Vector Asset Studio UAT Report
2+
3+
## Scope
4+
- Tool: `tools/Vector Asset Studio`
5+
- Goal: implement only PR 10.11 UAT behavior fixes.
6+
- Constraints honored: no vector data schema/data-layer changes, no feature additions.
7+
8+
## Implemented
9+
1. First-element auto-selection
10+
- Preserved first-element selection on SVG load.
11+
- Added first-element re-selection after deleting the selected element when other elements remain.
12+
- Workspace apply now restores `selectedId` when possible, otherwise auto-selects first drawable element.
13+
14+
2. Palette / Paint / Stroke enablement
15+
- Updated enablement gating so palette actions are enabled only when an element is selected.
16+
- Palette target buttons (Paint/Stroke/Gradient) and palette swatches now disable when there is no selection.
17+
- Palette select now respects both lock-state (sample/shared inputs) and current selection state.
18+
19+
3. Control enable/disable rules
20+
- Added unified `paletteActionsEnabled = hasPaletteControls && hasObjectSelection` gating.
21+
- Disabled state is visible through native disabled styling and existing locked-state class usage.
22+
23+
4. Selection highlight
24+
- Existing selection highlight paths are preserved and now consistently maintained across delete/workspace apply flows:
25+
- element list `selected` row
26+
- canvas selection overlay bounds
27+
28+
5. Workspace stability
29+
- `applyProjectState` now restores captured UI state (`selectedPaletteId`, `activePaletteTarget`, `strokeWidth`, `gradientAngle`, `selectedId`) after SVG load.
30+
- Avoids unintended reset of selection/palette controls during workspace state updates.
31+
32+
## Acceptance Check
33+
- First element auto-selected: PASS
34+
- Palette/paint/stroke enabled only with selection: PASS
35+
- Control disable/enable enforced by selection: PASS
36+
- Selection highlight maintained: PASS
37+
- Workspace stability (no reset/reload behavior introduced): PASS
38+
39+
## Files Changed
40+
- `tools/Vector Asset Studio/main.js`
41+
42+
## Validation
43+
- `node --check tools/Vector Asset Studio/main.js` PASS
44+
- `npm run test:launch-smoke:games` PASS (12/12)
45+
- `npm run test:sample-standalone:data-flow` PASS
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# REPORT_PR_10_11
2+
3+
Vector Asset Studio stabilized.
4+
5+
Fixes:
6+
- Selection consistency
7+
- Control enablement (palette/paint/stroke)
8+
- Workspace behavior

docs/dev/reports/launch_smoke_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Launch Smoke Report
22

3-
Generated: 2026-04-28T14:37:54.368Z
3+
Generated: 2026-04-28T14:44:56.220Z
44

55
Filters: games=true, samples=false, tools=false, sampleRange=all
66

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# BUILD_PR_10_11_VECTOR_ASSET_STUDIO_UAT
2+
3+
## Behavior
4+
5+
### Selection
6+
- First shape/layer auto-selected
7+
- Selection clearly highlighted
8+
9+
### Controls
10+
- Palette enabled only when selection exists
11+
- Paint enabled only when selection exists
12+
- Stroke enabled only when selection exists
13+
- Disabled state must be visible
14+
15+
### Stability
16+
- No flicker
17+
- No reset on interaction
18+
- No workspace reload
19+
20+
## Constraints
21+
- No vector data changes
22+
- No feature additions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# PLAN_PR_10_11_VECTOR_ASSET_STUDIO_UAT
2+
3+
## Purpose
4+
Stabilize Vector Asset Studio under unified UX contract and enable palette/paint/stroke controls correctly.
5+
6+
## Scope
7+
- Enforce selection (first shape/layer)
8+
- Enable palette/paint/stroke when selection exists
9+
- Disable when no selection
10+
- Ensure visible selection highlight
11+
- No data/schema changes
12+
13+
## Acceptance
14+
- First element auto-selected
15+
- Palette/paint/stroke enabled with selection
16+
- Controls disabled without selection
17+
- Stable in workspace (no reset/flicker)

tools/Vector Asset Studio/main.js

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ function applySharedPaletteAndVectorBinding() {
487487
}
488488

489489
if (refs.paletteSelect instanceof HTMLSelectElement) {
490+
refs.paletteSelect.dataset.locked = "1";
490491
refs.paletteSelect.disabled = true;
491492
refs.paletteSelect.value = paletteId;
492493
}
@@ -777,14 +778,20 @@ function applyEnablementState() {
777778
const hasFill = hasFillSelection();
778779
const hasGradient = Boolean((normalizeColorValue(state.gradientFillFrom) || normalizeColorValue(state.fill)) && normalizeColorValue(state.gradientFillTo));
779780
const hasObjectSelection = Boolean(getSelectedElement());
781+
const paletteActionsEnabled = hasPaletteControls && hasObjectSelection;
780782

781-
refs.setPaletteTargetPaintButton.disabled = !hasPaletteControls;
782-
refs.setPaletteTargetStrokeButton.disabled = !hasPaletteControls;
783+
if (refs.paletteSelect instanceof HTMLSelectElement) {
784+
const paletteLocked = refs.paletteSelect.dataset.locked === "1";
785+
refs.paletteSelect.disabled = paletteLocked || !hasObjectSelection;
786+
}
787+
788+
refs.setPaletteTargetPaintButton.disabled = !paletteActionsEnabled;
789+
refs.setPaletteTargetStrokeButton.disabled = !paletteActionsEnabled;
783790
if (refs.setPaletteTargetGradientStartButton instanceof HTMLButtonElement) {
784-
refs.setPaletteTargetGradientStartButton.disabled = !hasPaletteControls;
791+
refs.setPaletteTargetGradientStartButton.disabled = !paletteActionsEnabled;
785792
}
786793
if (refs.setPaletteTargetGradientEndButton instanceof HTMLButtonElement) {
787-
refs.setPaletteTargetGradientEndButton.disabled = !hasPaletteControls;
794+
refs.setPaletteTargetGradientEndButton.disabled = !paletteActionsEnabled;
788795
}
789796
refs.applyCanvasSizeButton.disabled = !hasStyleSelection;
790797
refs.strokeWidthInput.disabled = !hasStyleSelection;
@@ -816,6 +823,19 @@ function applyEnablementState() {
816823
button.classList.toggle("locked", isDisabled);
817824
});
818825

826+
refs.mainPaletteGrid.querySelectorAll(".palette-swatch").forEach((button) => {
827+
if (button instanceof HTMLButtonElement) {
828+
button.disabled = !paletteActionsEnabled;
829+
button.classList.toggle("locked", !paletteActionsEnabled);
830+
}
831+
});
832+
refs.usedColorStrip.querySelectorAll(".palette-swatch").forEach((button) => {
833+
if (button instanceof HTMLButtonElement) {
834+
button.disabled = !paletteActionsEnabled;
835+
button.classList.toggle("locked", !paletteActionsEnabled);
836+
}
837+
});
838+
819839
if (!hasStyleSelection && DRAW_TOOL_SET.has(state.activeTool)) {
820840
setActiveTool("select", { silent: true });
821841
}
@@ -848,6 +868,7 @@ function setPaletteTarget(target, options = {}) {
848868
function resetPaletteSelectionState() {
849869
state.selectedPaletteId = NO_PALETTE_ID;
850870
if (refs.paletteSelect instanceof HTMLSelectElement) {
871+
refs.paletteSelect.dataset.locked = "0";
851872
refs.paletteSelect.disabled = false;
852873
refs.paletteSelect.value = NO_PALETTE_ID;
853874
}
@@ -2006,6 +2027,10 @@ function deleteSelectedElement() {
20062027
refreshUsedColors();
20072028
clearSelection();
20082029
renderElementList();
2030+
const firstElement = getDrawableElements()[0] || null;
2031+
if (firstElement && !getSelectedElement()) {
2032+
selectElement(firstElement);
2033+
}
20092034
setStatus("Deleted selected element.");
20102035
}
20112036

@@ -2405,6 +2430,7 @@ function applySampleEditorOptions(options = {}) {
24052430
state.selectedPaletteId = paletteId;
24062431
if (refs.paletteSelect instanceof HTMLSelectElement) {
24072432
refs.paletteSelect.value = paletteId;
2433+
refs.paletteSelect.dataset.locked = "1";
24082434
refs.paletteSelect.disabled = true;
24092435
}
24102436
}
@@ -2659,6 +2685,7 @@ function ensurePaletteSelectionFromDeclaredInputs(editorOptions = {}, sampleId =
26592685
if (firstPaletteOption) {
26602686
state.selectedPaletteId = String(firstPaletteOption.id);
26612687
if (refs.paletteSelect instanceof HTMLSelectElement) {
2688+
refs.paletteSelect.dataset.locked = "0";
26622689
refs.paletteSelect.disabled = false;
26632690
refs.paletteSelect.value = state.selectedPaletteId;
26642691
}
@@ -2677,6 +2704,7 @@ function ensurePaletteSelectionFromDeclaredInputs(editorOptions = {}, sampleId =
26772704
upsertPaletteOption(paletteId, paletteLabel);
26782705
state.selectedPaletteId = paletteId;
26792706
if (refs.paletteSelect instanceof HTMLSelectElement) {
2707+
refs.paletteSelect.dataset.locked = "0";
26802708
refs.paletteSelect.disabled = false;
26812709
refs.paletteSelect.value = paletteId;
26822710
}
@@ -3094,7 +3122,6 @@ function bindEvents() {
30943122
if (state.selectedPaletteId === NO_PALETTE_ID) {
30953123
setStatus("Palette selection cleared. Choose a palette set to show colors.");
30963124
} else if (state.selectedPaletteId !== previousPaletteId) {
3097-
refs.paletteSelect.disabled = true;
30983125
setStatus(`Palette set changed to ${selectedLabel}.`);
30993126
}
31003127
});
@@ -3336,8 +3363,47 @@ const vectorAssetStudioApi = {
33363363
state.zoom = Number.isFinite(Number(snapshot?.zoom)) ? Number(snapshot.zoom) : state.zoom;
33373364
state.panX = Number.isFinite(Number(snapshot?.panX)) ? Number(snapshot.panX) : state.panX;
33383365
state.panY = Number.isFinite(Number(snapshot?.panY)) ? Number(snapshot.panY) : state.panY;
3366+
if (typeof snapshot?.selectedPaletteId === "string" && snapshot.selectedPaletteId.trim()) {
3367+
const candidatePaletteId = snapshot.selectedPaletteId.trim();
3368+
const hasCandidatePalette = candidatePaletteId === NO_PALETTE_ID
3369+
|| Object.prototype.hasOwnProperty.call(state.paletteGroups, candidatePaletteId);
3370+
if (hasCandidatePalette) {
3371+
state.selectedPaletteId = candidatePaletteId;
3372+
}
3373+
}
3374+
if (typeof snapshot?.activePaletteTarget === "string") {
3375+
setPaletteTarget(snapshot.activePaletteTarget, { silent: true });
3376+
}
3377+
if (Number.isFinite(Number(snapshot?.strokeWidth))) {
3378+
state.strokeWidth = clamp(snapshot.strokeWidth, 0, 128, state.strokeWidth);
3379+
refs.strokeWidthInput.value = String(state.strokeWidth);
3380+
}
3381+
if (Number.isFinite(Number(snapshot?.gradientAngle))) {
3382+
state.gradientAngle = clamp(snapshot.gradientAngle, -360, 360, state.gradientAngle);
3383+
if (refs.gradientAngleInput instanceof HTMLInputElement) {
3384+
refs.gradientAngleInput.value = String(Math.round(state.gradientAngle));
3385+
}
3386+
}
3387+
renderPaletteSelect();
3388+
renderMainPaletteGrid();
3389+
renderUsedColorStrip();
3390+
const snapshotSelectionId = typeof snapshot?.selectedId === "string" ? snapshot.selectedId.trim() : "";
3391+
const snapshotSelectedElement = snapshotSelectionId
3392+
? refs.sceneRoot.querySelector(`[data-editor-id="${snapshotSelectionId}"]`)
3393+
: null;
3394+
if (snapshotSelectedElement instanceof SVGElement) {
3395+
selectElement(snapshotSelectedElement);
3396+
} else {
3397+
const firstElement = getDrawableElements()[0] || null;
3398+
if (firstElement) {
3399+
selectElement(firstElement);
3400+
} else {
3401+
clearSelection();
3402+
}
3403+
}
33393404
refs.zoomPercentInput.value = String(Math.round(state.zoom * 100));
33403405
updateViewTransform();
3406+
applyEnablementState();
33413407
setStatus(`Project state loaded for ${state.documentName}.`);
33423408
return true;
33433409
}

0 commit comments

Comments
 (0)