Skip to content

Commit 2a79ccb

Browse files
author
DavidQ
committed
skin-editor+breakout: tighten shape/type behavior and workspace handoff
add/label updates: Walls (1-4), Flattened type support, disable Add when Flattened is selected flatten rules: block flatten selection for HUD Color objects (disabled gray checkbox), keep flatten-only creation path rendering fixes: draw oval with ellipse, preserve oval shape on create, use single Size control for square and lock width/height object workflow: add Move Up/Down ordering controls and apply object order to flatten preview/stacking palette row format: reorder to <symbol> <name> <#xxxxxx> Breakout handoff/data: mark wall as shape: "flattened" in preset/default skin and normalize workspace catalog paths for proper workspace pass-through
1 parent fbbc409 commit 2a79ccb

5 files changed

Lines changed: 123 additions & 13 deletions

File tree

games/breakout/assets/presets/game-breakout-skin-editor.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"color": "#000000"
2020
},
2121
"wall": {
22+
"shape": "flattened",
2223
"color": "#f8f8f2",
2324
"thickness": 16
2425
},

games/breakout/assets/skins/default.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"color": "#000000"
1010
},
1111
"wall": {
12+
"shape": "flattened",
1213
"color": "#f8f8f2",
1314
"thickness": 16
1415
},

games/breakout/assets/workspace.asset-catalog.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
"gameId": "breakout",
55
"assets": {
66
"palette.breakout.classic": {
7-
"path": "/games/breakout/assets/palettes/breakout-classic.palette.json",
7+
"path": "/games/Breakout/assets/palettes/breakout-classic.palette.json",
88
"kind": "palette",
99
"source": "workspace-manager"
1010
},
1111
"skin.main": {
12-
"path": "/games/breakout/assets/skins/default.json",
12+
"path": "/games/Breakout/assets/skins/default.json",
1313
"kind": "skin",
1414
"source": "workspace-manager"
1515
}

tools/Skin Editor/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ <h4>Object</h4>
5353
<option value="polygon">Polygon</option>
5454
<option value="star">Star</option>
5555
<option value="ring">Ring</option>
56-
<option value="wall-3-sides">Wall (3 Sides)</option>
56+
<option value="flattened">Flattened</option>
57+
<option value="wall-multi-side">Walls (1-4)</option>
5758
<option value="hud-color">HUD Color</option>
5859
</select>
5960
</div>

tools/Skin Editor/main.js

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,24 @@ function getSelectedObjectKeysInObjectOrder() {
156156
return keys.filter((key) => selectedKeySet.has(key));
157157
}
158158

159+
function getSelectedShapeTypeValue() {
160+
return refs.newShapeType instanceof HTMLSelectElement
161+
? normalizeText(refs.newShapeType.value).toLowerCase()
162+
: "";
163+
}
164+
165+
function updateAddShapeButtonState() {
166+
if (!(refs.addShapeButton instanceof HTMLButtonElement)) {
167+
return;
168+
}
169+
const selectedType = getSelectedShapeTypeValue();
170+
const disabled = selectedType === "flattened";
171+
refs.addShapeButton.disabled = disabled;
172+
refs.addShapeButton.title = disabled
173+
? "Flattened objects are created only with the Flatten button."
174+
: "";
175+
}
176+
159177
function updateFlattenButtonState() {
160178
const validSelection = getValidSelectedObjectKeys();
161179
state.selectedObjectKeys = validSelection;
@@ -421,6 +439,12 @@ function createShapePreset(shapeType) {
421439
color: "#f8f8f2"
422440
};
423441
}
442+
if (type === "flattened") {
443+
return {
444+
shape: "flattened",
445+
components: []
446+
};
447+
}
424448
if (type === "polygon") {
425449
return {
426450
shape: "polygon",
@@ -438,7 +462,7 @@ function createShapePreset(shapeType) {
438462
innerRadius: 26
439463
};
440464
}
441-
if (type === "wall-3-sides") {
465+
if (type === "wall-multi-side") {
442466
return {
443467
shape: "wall",
444468
color: "#f8f8f2",
@@ -453,7 +477,12 @@ function createShapePreset(shapeType) {
453477
return { color: "#f8f8f2", radius: 42 };
454478
}
455479
if (type === "oval") {
456-
return { color: "#f8f8f2", width: 140, height: 90 };
480+
return {
481+
shape: "oval",
482+
color: "#f8f8f2",
483+
width: 140,
484+
height: 90
485+
};
457486
}
458487
if (type === "square") {
459488
return { color: "#f8f8f2", width: 96, height: 96 };
@@ -565,12 +594,14 @@ function inferShapeTypeFromSelectedObject() {
565594
}
566595

567596
function inferShapeTypeFromObjectKey(objectKey) {
597+
const game = getSelectedGameOption();
598+
const normalizedGameId = normalizeText(game?.id).toLowerCase();
568599
const selectedObject = toObject(state.activeSkin?.objects?.[objectKey]);
569600
const shape = normalizeText(selectedObject.shape).toLowerCase();
570601
if (shape === "wall") {
571-
return "wall-3-sides";
602+
return "wall-multi-side";
572603
}
573-
if (shape && ["circle", "oval", "rectangle", "square", "polygon", "star", "ring", "hud-color"].includes(shape)) {
604+
if (shape && ["circle", "oval", "rectangle", "square", "polygon", "star", "ring", "flattened", "hud-color"].includes(shape)) {
574605
return shape;
575606
}
576607
const hasSides = Number.isFinite(Number(selectedObject.sides));
@@ -580,13 +611,14 @@ function inferShapeTypeFromObjectKey(objectKey) {
580611
const hasWidth = Number.isFinite(Number(selectedObject.width));
581612
const hasHeight = Number.isFinite(Number(selectedObject.height));
582613
const hasThickness = Number.isFinite(Number(selectedObject.thickness));
614+
const hasComponents = Array.isArray(selectedObject.components);
583615
const hasWallFlags = ["left", "right", "top", "bottom"].some((key) => typeof selectedObject[key] === "boolean");
584616
const colorPropertyCount = Object.values(selectedObject)
585617
.filter((value) => typeof value === "string" && parseHexForPicker(value))
586618
.length;
587619

588620
if (hasThickness && hasWallFlags) {
589-
return "wall-3-sides";
621+
return "wall-multi-side";
590622
}
591623
if (hasSides && hasOuter && hasInner) {
592624
return "star";
@@ -597,6 +629,12 @@ function inferShapeTypeFromObjectKey(objectKey) {
597629
if (hasOuter && hasInner) {
598630
return "ring";
599631
}
632+
if (normalizedGameId === "breakout" && normalizeText(objectKey).toLowerCase() === "wall" && hasThickness) {
633+
return "flattened";
634+
}
635+
if (hasComponents) {
636+
return "flattened";
637+
}
600638
if (hasRadius && hasWidth && hasHeight) {
601639
return "oval";
602640
}
@@ -626,6 +664,7 @@ function syncShapeTypeControlFromSelection() {
626664
if (optionExists) {
627665
refs.newShapeType.value = inferredShapeType;
628666
}
667+
updateAddShapeButtonState();
629668
}
630669

631670
function syncSelectedObjectUiFromSelection() {
@@ -677,6 +716,23 @@ function setObjectPropertyValue(objectKey, propertyKey, value) {
677716
const nextValue = typeof value === "number"
678717
? clampNumericProperty(propertyKey, value)
679718
: value;
719+
const normalizedPropertyKey = normalizeText(propertyKey).toLowerCase();
720+
const isSquareObject = inferShapeTypeFromObjectKey(objectKey) === "square";
721+
if (isSquareObject
722+
&& typeof nextValue === "number"
723+
&& Number.isFinite(nextValue)
724+
&& ["size", "width", "height"].includes(normalizedPropertyKey)) {
725+
const squareSize = clampNumericProperty("size", nextValue);
726+
state.activeSkin.objects[objectKey].size = squareSize;
727+
state.activeSkin.objects[objectKey].width = squareSize;
728+
state.activeSkin.objects[objectKey].height = squareSize;
729+
updateEditorFromState("visual-editor");
730+
syncSelectedObjectUiFromSelection();
731+
renderPaletteList();
732+
renderObjectControls();
733+
drawSelectedObjectPreview();
734+
return;
735+
}
680736
state.activeSkin.objects[objectKey][propertyKey] = nextValue;
681737
updateEditorFromState("visual-editor");
682738
syncSelectedObjectUiFromSelection();
@@ -735,10 +791,10 @@ function renderPaletteList() {
735791
}
736792
const swatchName = normalizeText(entry?.name) || `Swatch ${index + 1}`;
737793
const swatchSymbol = normalizeText(entry?.symbol);
738-
const suffix = swatchSymbol ? ` [${swatchSymbol}]` : "";
794+
const prefix = swatchSymbol ? `${swatchSymbol} ` : "";
739795
return {
740796
id: `${sharedPalette?.paletteId || "shared-palette"}.${index}`,
741-
label: `${swatchName}${suffix}`,
797+
label: `${prefix}${swatchName}`,
742798
color
743799
};
744800
})
@@ -858,6 +914,7 @@ function renderObjectControls() {
858914

859915
const selectedKey = state.selectedObjectKey;
860916
const selectedObject = toObject(state.activeSkin?.objects?.[selectedKey]);
917+
const inferredShapeType = inferShapeTypeFromObjectKey(selectedKey);
861918
const entries = Object.entries(selectedObject);
862919
if (!selectedKey || entries.length === 0) {
863920
const note = document.createElement("p");
@@ -867,10 +924,39 @@ function renderObjectControls() {
867924
return;
868925
}
869926

927+
if (inferredShapeType === "square") {
928+
const sizeCandidates = [Number(selectedObject.size), Number(selectedObject.width), Number(selectedObject.height)]
929+
.filter((entry) => Number.isFinite(entry));
930+
const sizeValue = clampNumericProperty("size", sizeCandidates.find((entry) => entry > 0) ?? sizeCandidates[0] ?? 96);
931+
const sizeRow = document.createElement("div");
932+
sizeRow.className = "skin-editor-row skin-editor-row--number";
933+
const sizeLabel = document.createElement("span");
934+
sizeLabel.className = "skin-editor-row-label";
935+
sizeLabel.textContent = "Size";
936+
const sizeInput = createBasicField("skin-editor-field", String(sizeValue));
937+
sizeInput.type = "number";
938+
sizeInput.step = numberStep(sizeValue);
939+
sizeInput.min = "1";
940+
sizeInput.addEventListener("input", () => {
941+
const parsed = clampNumericProperty("size", Number(sizeInput.value));
942+
if (!Number.isFinite(parsed)) {
943+
return;
944+
}
945+
sizeInput.value = String(parsed);
946+
setObjectPropertyValue(selectedKey, "size", parsed);
947+
});
948+
sizeRow.appendChild(sizeLabel);
949+
sizeRow.appendChild(sizeInput);
950+
refs.objectControls.appendChild(sizeRow);
951+
}
952+
870953
entries.forEach(([propertyKey, propertyValue]) => {
871954
if (propertyKey === "shape") {
872955
return;
873956
}
957+
if (inferredShapeType === "square" && ["width", "height", "size"].includes(normalizeText(propertyKey).toLowerCase())) {
958+
return;
959+
}
874960
if (typeof propertyValue === "number" && Number.isFinite(propertyValue)) {
875961
const minValue = /sides?/i.test(normalizeText(propertyKey))
876962
? 3
@@ -1029,6 +1115,20 @@ function drawSelectedObjectPreview() {
10291115
context.fill();
10301116
return;
10311117
}
1118+
if (shapeType === "oval") {
1119+
context.beginPath();
1120+
context.ellipse(
1121+
centerX,
1122+
centerY,
1123+
Math.max(6, width / 2),
1124+
Math.max(6, height / 2),
1125+
0,
1126+
0,
1127+
Math.PI * 2
1128+
);
1129+
context.fill();
1130+
return;
1131+
}
10321132
if (shapeType === "ring" || (Number.isFinite(Number(object.outerRadius)) && Number.isFinite(Number(object.innerRadius)))) {
10331133
context.beginPath();
10341134
context.arc(centerX, centerY, outerRadius, 0, Math.PI * 2);
@@ -1271,9 +1371,12 @@ function addShapeFromControls() {
12711371
setStatus("Load a skin before adding a shape.");
12721372
return;
12731373
}
1274-
const selectedType = refs.newShapeType instanceof HTMLSelectElement
1275-
? normalizeText(refs.newShapeType.value).toLowerCase()
1276-
: "rectangle";
1374+
const selectedType = getSelectedShapeTypeValue() || "rectangle";
1375+
if (selectedType === "flattened") {
1376+
updateAddShapeButtonState();
1377+
setStatus("Flattened objects are created only with the Flatten button.");
1378+
return;
1379+
}
12771380
const typedName = refs.newShapeName instanceof HTMLInputElement
12781381
? refs.newShapeName.value
12791382
: "";
@@ -1517,6 +1620,7 @@ async function loadPresetFromQuery() {
15171620
}
15181621

15191622
function bindEvents() {
1623+
updateAddShapeButtonState();
15201624
updateFlattenButtonState();
15211625
updateObjectOrderButtonState();
15221626
refs.loadButton?.addEventListener("click", () => {
@@ -1533,6 +1637,9 @@ function bindEvents() {
15331637
refs.howToUseButton?.addEventListener("click", () => {
15341638
window.location.href = "./how_to_use.html";
15351639
});
1640+
refs.newShapeType?.addEventListener("change", () => {
1641+
updateAddShapeButtonState();
1642+
});
15361643
refs.syncVisualButton?.addEventListener("click", syncVisualFromJson);
15371644
refs.addShapeButton?.addEventListener("click", addShapeFromControls);
15381645
refs.renameObjectButton?.addEventListener("click", renameObjectFromControls);

0 commit comments

Comments
 (0)