Skip to content

Commit 9e95969

Browse files
committed
feat: add rotation for cables
1 parent 432cae4 commit 9e95969

8 files changed

Lines changed: 329 additions & 71 deletions

File tree

client/src/components/creator/creator-board/creator-tile.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ import {
22
EXTRA_HEIGHT,
33
TALL_WALL_HEIGHT_MULTIPLIER,
44
} from "../../../constants/global";
5-
import {
6-
ALL_ITEMS_MAP,
7-
type LayerItem,
8-
} from "../../../constants/layer-items";
5+
import { ALL_ITEMS_MAP, type LayerItem } from "../../../constants/layer-items";
96
import { getTileBackgroundData } from "../../../utils/tileset-utils";
107
import { renderTilesetLayer } from "../shared/render-tileset-layer";
118

@@ -39,10 +36,8 @@ export function CreatorTile({ sizePx, tileKeys }: CreatorTileProps) {
3936
);
4037
}
4138

42-
/** Background slot (index 1) drives wall stacking / tall layout. */
4339
const wallBg = resolvedItems[1];
44-
const isWallCell =
45-
wallBg?.key === "w1t" || wallBg?.key === "w2t";
40+
const isWallCell = wallBg?.key === "w1t" || wallBg?.key === "w2t";
4641

4742
return (
4843
<div
@@ -70,7 +65,9 @@ export function CreatorTile({ sizePx, tileKeys }: CreatorTileProps) {
7065
isWallCell || isTall ? 0 : `${sizePx * EXTRA_HEIGHT}px`;
7166

7267
const useCompositeBlend =
73-
item.baseFrame !== undefined || Boolean(item.color);
68+
item.baseFrame !== undefined ||
69+
Boolean(item.color) ||
70+
(item.rotationDeg != null && item.rotationDeg % 360 !== 0);
7471

7572
if (isEntity) {
7673
return (
@@ -104,15 +101,19 @@ export function CreatorTile({ sizePx, tileKeys }: CreatorTileProps) {
104101
left: 0,
105102
width: `${sourceTileSizePx}px`,
106103
height: `${sourceTileSizePx * TALL_WALL_HEIGHT_MULTIPLIER}px`,
107-
backgroundImage: bgUrl,
108-
backgroundPosition: `${bgPosX}px ${bgPosY}px`,
109-
backgroundRepeat: "no-repeat",
110-
imageRendering: "pixelated",
111104
transform: `scale(${scale})`,
112105
transformOrigin: "top left",
113106
pointerEvents: "none",
114107
}}
115-
/>
108+
>
109+
{renderTilesetLayer({
110+
frameId: item.frame,
111+
tileSizePx: sourceTileSizePx,
112+
heightMultiplier: TALL_WALL_HEIGHT_MULTIPLIER,
113+
withWallYOffset: true,
114+
rotationDeg: item.rotationDeg,
115+
})}
116+
</div>
116117
);
117118
}
118119

@@ -134,6 +135,7 @@ export function CreatorTile({ sizePx, tileKeys }: CreatorTileProps) {
134135
renderTilesetLayer({
135136
frameId: item.baseFrame,
136137
direction: item.direction,
138+
rotationDeg: item.rotationDeg,
137139
tileSizePx: sourceTileSizePx,
138140
heightMultiplier: TALL_WALL_HEIGHT_MULTIPLIER,
139141
withWallYOffset: true,
@@ -142,6 +144,7 @@ export function CreatorTile({ sizePx, tileKeys }: CreatorTileProps) {
142144
frameId: item.frame,
143145
color: item.color,
144146
direction: item.direction,
147+
rotationDeg: item.rotationDeg,
145148
tileSizePx: sourceTileSizePx,
146149
heightMultiplier: TALL_WALL_HEIGHT_MULTIPLIER,
147150
withWallYOffset: true,

client/src/components/creator/creator-items-select/creator-items-select.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { useState } from "react";
22

33
import { LAYER_NAMES } from "../../../constants/global";
4-
import { LAYER_ITEMS, type LayerItem } from "../../../constants/layer-items";
4+
import type {
5+
FloorDecoyRotationDeg,
6+
LayerItem,
7+
} from "../../../constants/layer-items";
8+
import {
9+
LAYER_ITEMS,
10+
creatorPaletteKeyForLookup,
11+
} from "../../../constants/layer-items";
512
import { CreatorLayerOptionsGrid } from "./parts/creator-layer-options-grid";
613
import { CreatorLayerTabs } from "./parts/creator-layer-tabs";
714
import { CreatorSelectedBlock } from "./parts/creator-selected-block";
@@ -17,11 +24,15 @@ const LAYER_TABS = [
1724
interface CreatorItemsSelectProps {
1825
activeBlock: LayerItem | null;
1926
setActiveBlock: (block: LayerItem | null) => void;
27+
floorCableRotationByBase: Record<string, FloorDecoyRotationDeg>;
28+
rotateCableAtBase: (baseKey: string) => void;
2029
}
2130

2231
export function CreatorItemsSelect({
2332
activeBlock,
2433
setActiveBlock,
34+
floorCableRotationByBase,
35+
rotateCableAtBase,
2536
}: CreatorItemsSelectProps) {
2637
const [selectedLayer, setSelectedLayer] = useState<string>(LAYER_TABS[0].key);
2738

@@ -38,8 +49,9 @@ export function CreatorItemsSelect({
3849
};
3950

4051
const findLayerForBlock = (blockKey: string): string | null => {
52+
const paletteKey = creatorPaletteKeyForLookup(blockKey);
4153
for (const [layerKey, layerItems] of Object.entries(LAYER_ITEMS)) {
42-
if (layerItems.some((item) => item.key === blockKey)) {
54+
if (layerItems.some((item) => item.key === paletteKey)) {
4355
return layerKey;
4456
}
4557
}
@@ -74,6 +86,8 @@ export function CreatorItemsSelect({
7486
layerKey={selectedLayer}
7587
activeBlock={activeBlock}
7688
setActiveBlock={setActiveBlock}
89+
floorCableRotationByBase={floorCableRotationByBase}
90+
rotateCableAtBase={rotateCableAtBase}
7791
/>
7892
</div>
7993
</div>

client/src/components/creator/creator-items-select/parts/creator-layer-options-grid.tsx

Lines changed: 102 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import { Palette } from "lucide-react";
1+
import { Palette, RotateCw } from "lucide-react";
22
import { useRef, useState } from "react";
33

44
import {
55
COLORABLE_BASE_ITEMS,
6+
type FloorDecoyRotationDeg,
67
type LayerItem,
8+
creatorPaletteKeyForLookup,
79
getColoredVariant,
810
getGridItems,
11+
getRotatedFloorPlacementItem,
12+
isRotatableFloorBaseKey,
13+
paletteDisplayLabel,
14+
parseRotatableFloorKey,
915
} from "../../../../constants/layer-items";
1016
import { getUIBlockBackgroundData } from "../../../../utils/tileset-utils";
1117
import { renderTilesetLayer } from "../../shared/render-tileset-layer";
@@ -15,12 +21,16 @@ interface CreatorLayerOptionsGridProps {
1521
layerKey: string;
1622
activeBlock: LayerItem | null;
1723
setActiveBlock: (block: LayerItem | null) => void;
24+
floorCableRotationByBase: Record<string, FloorDecoyRotationDeg>;
25+
rotateCableAtBase: (baseKey: string) => void;
1826
}
1927

2028
export function CreatorLayerOptionsGrid({
2129
layerKey,
2230
activeBlock,
2331
setActiveBlock,
32+
floorCableRotationByBase,
33+
rotateCableAtBase,
2434
}: CreatorLayerOptionsGridProps) {
2535
const items = getGridItems(layerKey);
2636

@@ -35,13 +45,24 @@ export function CreatorLayerOptionsGrid({
3545
const pickerAnchorRef = useRef<HTMLDivElement | null>(null);
3646

3747
const getDisplayItem = (item: LayerItem): LayerItem => {
38-
if (!item.colorable || !item.baseKey) return item;
39-
const colorIdx = selectedColors[item.baseKey] ?? 0;
40-
return getColoredVariant(item.baseKey, colorIdx) ?? item;
48+
let out = item;
49+
if (out.colorable && out.baseKey) {
50+
const colorIdx = selectedColors[out.baseKey] ?? 0;
51+
out = getColoredVariant(out.baseKey, colorIdx) ?? out;
52+
}
53+
if (item.supportsRotation && isRotatableFloorBaseKey(item.key)) {
54+
const deg = floorCableRotationByBase[item.key] ?? 0;
55+
return getRotatedFloorPlacementItem(item.key, deg) ?? out;
56+
}
57+
return out;
4158
};
4259

4360
const isItemActive = (item: LayerItem): boolean => {
4461
if (!activeBlock) return false;
62+
if (item.supportsRotation) {
63+
const p = parseRotatableFloorKey(activeBlock.key);
64+
return p?.baseKey === item.key;
65+
}
4566
if (item.colorable && item.baseKey) {
4667
return activeBlock.baseKey === item.baseKey;
4768
}
@@ -67,6 +88,16 @@ export function CreatorLayerOptionsGrid({
6788
setOpenPickerBaseKey((prev) => (prev === baseKey ? null : baseKey));
6889
};
6990

91+
const handleRotateClick = (e: React.MouseEvent, baseKey: string) => {
92+
e.stopPropagation();
93+
if (isRotatableFloorBaseKey(baseKey)) {
94+
rotateCableAtBase(baseKey);
95+
}
96+
};
97+
98+
const paletteRowKeyForItem = (item: LayerItem) =>
99+
item.colorable && item.baseKey ? item.baseKey : item.key;
100+
70101
return (
71102
<div className="custom-scrollbar flex min-h-0 flex-1 flex-wrap content-start items-start justify-center gap-4 overflow-y-auto rounded-lg bg-violet-900/40 p-4">
72103
{items.map((item) => {
@@ -75,23 +106,45 @@ export function CreatorLayerOptionsGrid({
75106
const isColorable = Boolean(item.colorable && item.baseKey);
76107
const hasVariants =
77108
isColorable && (COLORABLE_BASE_ITEMS[item.baseKey!]?.length ?? 0) > 1;
109+
const canRotate =
110+
Boolean(item.supportsRotation) && isRotatableFloorBaseKey(item.key);
111+
112+
const paletteRowKey = creatorPaletteKeyForLookup(
113+
paletteRowKeyForItem(item),
114+
);
78115

79116
const useCompositeBlend =
80117
displayItem.baseFrame !== undefined || Boolean(displayItem.color);
81118

119+
const needsRotatePreview =
120+
displayItem.rotationDeg != null &&
121+
displayItem.rotationDeg % 360 !== 0;
122+
123+
const previewUsesTilesetLayers =
124+
useCompositeBlend || needsRotatePreview;
125+
82126
const { bgUrl, bgPosX, bgPosY } = getUIBlockBackgroundData(
83127
displayItem.frame,
84128
24,
85129
import.meta.env.BASE_URL,
86130
);
87131

132+
const paletteName = paletteDisplayLabel(displayItem);
133+
const rotatedKey = parseRotatableFloorKey(displayItem.key);
134+
const tileHoverTitle =
135+
rotatedKey !== null &&
136+
rotatedKey.rotationDeg % 360 !== 0 &&
137+
item.supportsRotation
138+
? `${paletteName} · ${rotatedKey.rotationDeg}°`
139+
: paletteName;
140+
88141
return (
89142
<div
90-
key={item.baseKey ?? item.key}
143+
key={paletteRowKey}
91144
ref={
92145
openPickerBaseKey === item.baseKey ? pickerAnchorRef : undefined
93146
}
94-
className="relative"
147+
className="relative w-[5rem] shrink-0"
95148
>
96149
<div
97150
role="button"
@@ -110,10 +163,10 @@ export function CreatorLayerOptionsGrid({
110163
? "bg-amber-400/30 ring-2 ring-amber-400"
111164
: "hover:bg-violet-500/40"
112165
}`}
113-
title={displayItem.label}
166+
title={tileHoverTitle}
114167
>
115168
<div className="relative h-20 w-20 overflow-hidden border-4 border-emerald-950 bg-blue-400">
116-
{!useCompositeBlend ? (
169+
{!previewUsesTilesetLayers ? (
117170
<div
118171
className="h-6 w-6"
119172
style={{
@@ -141,26 +194,54 @@ export function CreatorLayerOptionsGrid({
141194
renderTilesetLayer({
142195
frameId: displayItem.baseFrame,
143196
direction: displayItem.direction,
197+
rotationDeg: displayItem.rotationDeg,
144198
})}
145199
{renderTilesetLayer({
146200
frameId: displayItem.frame,
147201
color: displayItem.color,
148202
direction: displayItem.direction,
203+
rotationDeg: displayItem.rotationDeg,
149204
})}
150205
</div>
151206
)}
152207

153-
{hasVariants && (
154-
<button
155-
type="button"
156-
onClick={(e) => {
157-
handleExpandClick(e, item.baseKey!);
158-
}}
159-
className="absolute bottom-0.5 right-0.5 flex h-5 w-5 cursor-pointer items-center justify-center rounded bg-violet-900/80 text-violet-200 transition-colors hover:bg-violet-700/90 hover:text-white"
160-
aria-label="Choose palette color"
161-
title="Choose color"
162-
>
163-
<Palette className="h-4 w-4" aria-hidden strokeWidth={2} /> </button>
208+
{(canRotate || hasVariants) && (
209+
<div className="absolute bottom-0.5 right-0.5 flex flex-row-reverse items-center gap-0.5">
210+
{hasVariants && (
211+
<button
212+
type="button"
213+
onClick={(e) => {
214+
handleExpandClick(e, item.baseKey!);
215+
}}
216+
className="flex h-5 w-5 cursor-pointer items-center justify-center rounded bg-violet-900/80 text-violet-200 transition-colors hover:bg-violet-700/90 hover:text-white"
217+
aria-label="Choose palette color"
218+
title="Choose color"
219+
>
220+
<Palette
221+
className="h-4 w-4"
222+
aria-hidden
223+
strokeWidth={2}
224+
/>
225+
</button>
226+
)}
227+
{canRotate && (
228+
<button
229+
type="button"
230+
onClick={(e) => {
231+
handleRotateClick(e, item.key);
232+
}}
233+
className="flex h-5 w-5 cursor-pointer items-center justify-center rounded bg-violet-900/80 text-violet-200 transition-colors hover:bg-violet-700/90 hover:text-white"
234+
aria-label="Rotate cable (R)"
235+
title="Rotate (R)"
236+
>
237+
<RotateCw
238+
className="h-4 w-4"
239+
aria-hidden
240+
strokeWidth={2}
241+
/>
242+
</button>
243+
)}
244+
</div>
164245
)}
165246

166247
{Boolean(isColorable && displayItem.color) && (
@@ -170,8 +251,8 @@ export function CreatorLayerOptionsGrid({
170251
/>
171252
)}
172253
</div>
173-
<span className="max-w-[96px] text-center text-[11px] leading-tight font-medium text-violet-200">
174-
{displayItem.label}
254+
<span className="block w-full text-balance break-words px-0.5 text-center text-[10px] leading-tight font-medium text-violet-200 hyphens-auto">
255+
{paletteName}
175256
</span>
176257
</div>
177258

client/src/components/creator/creator-items-select/parts/creator-selected-block.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { LayerItem } from "../../../../constants/layer-items";
1+
import {
2+
type LayerItem,
3+
paletteDisplayLabel,
4+
} from "../../../../constants/layer-items";
25
import { getUIBlockBackgroundData } from "../../../../utils/tileset-utils";
36
import { renderTilesetLayer } from "../../shared/render-tileset-layer";
47

@@ -31,7 +34,9 @@ export function CreatorSelectedBlock({
3134
{(() => {
3235
const composite =
3336
activeBlock.baseFrame !== undefined ||
34-
Boolean(activeBlock.color);
37+
Boolean(activeBlock.color) ||
38+
(activeBlock.rotationDeg != null &&
39+
activeBlock.rotationDeg % 360 !== 0);
3540

3641
if (!composite) {
3742
const { bgUrl, bgPosX, bgPosY } = getUIBlockBackgroundData(
@@ -70,18 +75,20 @@ export function CreatorSelectedBlock({
7075
renderTilesetLayer({
7176
frameId: activeBlock.baseFrame,
7277
direction: activeBlock.direction,
78+
rotationDeg: activeBlock.rotationDeg,
7379
})}
7480
{renderTilesetLayer({
7581
frameId: activeBlock.frame,
7682
color: activeBlock.color,
7783
direction: activeBlock.direction,
84+
rotationDeg: activeBlock.rotationDeg,
7885
})}
7986
</div>
8087
);
8188
})()}
8289
</div>
83-
<span className="text-sm font-bold text-violet-50">
84-
{activeBlock.label}
90+
<span className="min-w-0 shrink text-sm font-bold break-words text-violet-50">
91+
{paletteDisplayLabel(activeBlock)}
8592
</span>
8693
{activeBlock.color && (
8794
<div

0 commit comments

Comments
 (0)