Skip to content
Merged
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
167 changes: 127 additions & 40 deletions src/lib/components/GlobalsPanel.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import type { CounterTrayParams, CustomShape } from '$lib/models/counterTray';
import type { CounterTrayParams, CustomShape, CustomBaseShape } from '$lib/models/counterTray';

interface Props {
params: CounterTrayParams;
Expand All @@ -8,6 +8,13 @@

let { params, onchange }: Props = $props();

const baseShapeOptions: { value: CustomBaseShape; label: string }[] = [
{ value: 'rectangle', label: 'Rectangle' },
{ value: 'square', label: 'Square' },
{ value: 'circle', label: 'Circle' },
{ value: 'hex', label: 'Hex' }
];

function updateParam<K extends keyof CounterTrayParams>(key: K, value: CounterTrayParams[K]) {
onchange({ ...params, [key]: value });
}
Expand All @@ -17,7 +24,10 @@
const newName = `Custom ${params.customShapes.length + 1}`;
onchange({
...params,
customShapes: [...params.customShapes, { name: newName, width: 20, length: 30 }]
customShapes: [
...params.customShapes,
{ name: newName, baseShape: 'rectangle', width: 20, length: 30 }
]
});
}

Expand Down Expand Up @@ -46,6 +56,34 @@
return;
}
}

// Handle baseShape changes - sync length to width for non-rectangle shapes
if (field === 'baseShape') {
const baseShape = value as CustomBaseShape;
if (baseShape !== 'rectangle') {
// For square, circle, hex: set length = width
newShapes[index] = { ...newShapes[index], baseShape, length: newShapes[index].width };
} else {
newShapes[index] = { ...newShapes[index], baseShape };
}
onchange({ ...params, customShapes: newShapes });
return;
}

// Handle width changes for non-rectangle shapes - sync length
if (field === 'width') {
const shape = newShapes[index];
const baseShape = shape.baseShape ?? 'rectangle';
if (baseShape !== 'rectangle') {
// For square, circle, hex: length = width
newShapes[index] = { ...newShapes[index], width: value as number, length: value as number };
} else {
newShapes[index] = { ...newShapes[index], width: value as number };
}
onchange({ ...params, customShapes: newShapes });
return;
}

newShapes[index] = { ...newShapes[index], [field]: value };
onchange({ ...params, customShapes: newShapes });
}
Expand Down Expand Up @@ -84,31 +122,24 @@

<section>
<h3 class="mb-2 text-xs font-semibold tracking-wide text-gray-400 uppercase">
Counter Dimensions
Simple Counters
</h3>
<div class="grid grid-cols-2 gap-2">
<label class="block">
<span class="text-xs text-gray-400">Square W</span>
<span class="text-xs text-gray-400">Square</span>
<input
type="number"
step="0.1"
value={params.squareWidth}
onchange={(e) => updateParam('squareWidth', parseFloat(e.currentTarget.value))}
onchange={(e) => {
const val = parseFloat(e.currentTarget.value);
onchange({ ...params, squareWidth: val, squareLength: val });
}}
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
</label>
<label class="block">
<span class="text-xs text-gray-400">Square L</span>
<input
type="number"
step="0.1"
value={params.squareLength}
onchange={(e) => updateParam('squareLength', parseFloat(e.currentTarget.value))}
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
</label>
<label class="block">
<span class="text-xs text-gray-400">Hex F-to-F</span>
<span class="text-xs text-gray-400">Hex (flat-to-flat)</span>
<input
type="number"
step="0.1"
Expand All @@ -118,7 +149,7 @@
/>
</label>
<label class="block">
<span class="text-xs text-gray-400">Circle Dia</span>
<span class="text-xs text-gray-400">Circle Diameter</span>
<input
type="number"
step="0.1"
Expand All @@ -144,7 +175,7 @@
onchange={(e) => updateParam('hexPointyTop', e.currentTarget.checked)}
class="rounded border-gray-600 bg-gray-700"
/>
<span class="text-xs text-gray-400">Hex Pointy</span>
<span class="text-xs text-gray-400">Hex Pointy Top</span>
</label>
</div>
</section>
Expand All @@ -155,6 +186,7 @@
</h3>
<div class="space-y-2">
{#each params.customShapes as shape, index (shape.name)}
{@const baseShape = shape.baseShape ?? 'rectangle'}
<div class="bg-gray-750 rounded p-2">
<div class="mb-2 flex items-center gap-2">
<input
Expand All @@ -172,32 +204,87 @@
&times;
</button>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mb-2">
<label class="block">
<span class="text-xs text-gray-400">Width</span>
<input
type="number"
step="0.1"
min="1"
value={shape.width}
onchange={(e) =>
updateCustomShape(index, 'width', parseFloat(e.currentTarget.value))}
<span class="text-xs text-gray-400">Base Shape</span>
<select
value={baseShape}
onchange={(e) => updateCustomShape(index, 'baseShape', e.currentTarget.value)}
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
</label>
<label class="block">
<span class="text-xs text-gray-400">Length</span>
<input
type="number"
step="0.1"
min="1"
value={shape.length}
onchange={(e) =>
updateCustomShape(index, 'length', parseFloat(e.currentTarget.value))}
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
>
{#each baseShapeOptions as option (option.value)}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</label>
</div>
<div class="grid grid-cols-2 gap-2">
{#if baseShape === 'rectangle'}
<label class="block">
<span class="text-xs text-gray-400">Width</span>
<input
type="number"
step="0.1"
min="1"
value={shape.width}
onchange={(e) =>
updateCustomShape(index, 'width', parseFloat(e.currentTarget.value))}
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
</label>
<label class="block">
<span class="text-xs text-gray-400">Length</span>
<input
type="number"
step="0.1"
min="1"
value={shape.length}
onchange={(e) =>
updateCustomShape(index, 'length', parseFloat(e.currentTarget.value))}
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
</label>
{:else if baseShape === 'square'}
<label class="col-span-2 block">
<span class="text-xs text-gray-400">Size</span>
<input
type="number"
step="0.1"
min="1"
value={shape.width}
onchange={(e) =>
updateCustomShape(index, 'width', parseFloat(e.currentTarget.value))}
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
</label>
{:else if baseShape === 'circle'}
<label class="col-span-2 block">
<span class="text-xs text-gray-400">Diameter</span>
<input
type="number"
step="0.1"
min="1"
value={shape.width}
onchange={(e) =>
updateCustomShape(index, 'width', parseFloat(e.currentTarget.value))}
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
</label>
{:else if baseShape === 'hex'}
<label class="col-span-2 block">
<span class="text-xs text-gray-400">Flat-to-Flat</span>
<input
type="number"
step="0.1"
min="1"
value={shape.width}
onchange={(e) =>
updateCustomShape(index, 'width', parseFloat(e.currentTarget.value))}
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
</label>
{/if}
</div>
</div>
{/each}
<button
Expand Down
32 changes: 20 additions & 12 deletions src/lib/components/TrayScene.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@
{#if stack.isEdgeLoaded}
<!-- Edge-loaded: counters standing on edge like books -->
{#each Array(stack.count) as _counterItem, counterIdx (counterIdx)}
{@const effectiveShape =
stack.shape === 'custom' ? (stack.customBaseShape ?? 'rectangle') : stack.shape}
{@const standingHeight =
stack.shape === 'custom'
? Math.min(stack.width, stack.length)
Expand All @@ -457,13 +459,13 @@
{@const counterSpacing = (stack.slotWidth ?? stack.count * stack.thickness) / stack.count}
{@const posX = meshOffset.x + stack.x + (counterIdx + 0.5) * counterSpacing}
{@const posZ = meshOffset.z - stack.y - (stack.slotDepth ?? stack.length) / 2}
{#if stack.shape === 'square' || stack.shape === 'custom'}
{#if effectiveShape === 'square' || effectiveShape === 'rectangle'}
<!-- Standing on edge: thickness along X (stacking), height along Y, length along Z -->
<T.Mesh position.x={posX} position.y={counterY} position.z={posZ}>
<T.BoxGeometry args={[stack.thickness, standingHeight, stack.length]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else if stack.shape === 'circle'}
{:else if effectiveShape === 'circle'}
<!-- Cylinder standing on edge: rotate so axis is along X -->
<T.Mesh
position.x={posX}
Expand Down Expand Up @@ -492,12 +494,12 @@
{@const counterSpacing = (stack.slotDepth ?? stack.count * stack.thickness) / stack.count}
{@const posX = meshOffset.x + stack.x + (stack.slotWidth ?? stack.length) / 2}
{@const posZ = meshOffset.z - stack.y - (counterIdx + 0.5) * counterSpacing}
{#if stack.shape === 'square' || stack.shape === 'custom'}
{#if effectiveShape === 'square' || effectiveShape === 'rectangle'}
<T.Mesh position.x={posX} position.y={counterY} position.z={posZ}>
<T.BoxGeometry args={[stack.length, standingHeight, stack.thickness]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else if stack.shape === 'circle'}
{:else if effectiveShape === 'circle'}
<T.Mesh
position.x={posX}
position.y={counterY}
Expand Down Expand Up @@ -531,12 +533,14 @@
{@const posZ = meshOffset.z - stack.y}
{@const isAlt = counterIdx % 2 === 1}
{@const counterColor = isAlt ? `hsl(${(stackIdx * 137.508) % 360}, 50%, 40%)` : stack.color}
{#if stack.shape === 'square' || stack.shape === 'custom'}
{@const effectiveShape =
stack.shape === 'custom' ? (stack.customBaseShape ?? 'rectangle') : stack.shape}
{#if effectiveShape === 'square' || effectiveShape === 'rectangle'}
<T.Mesh position.x={posX} position.y={posY} position.z={posZ}>
<T.BoxGeometry args={[stack.width, stack.thickness, stack.length]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else if stack.shape === 'circle'}
{:else if effectiveShape === 'circle'}
<T.Mesh position.x={posX} position.y={posY} position.z={posZ}>
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 32]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
Expand Down Expand Up @@ -579,6 +583,8 @@
{#if stack.isEdgeLoaded}
<!-- Edge-loaded: counters standing on edge like books -->
{#each Array(stack.count) as _counterItem, counterIdx (counterIdx)}
{@const effectiveShape =
stack.shape === 'custom' ? (stack.customBaseShape ?? 'rectangle') : stack.shape}
{@const standingHeight =
stack.shape === 'custom'
? Math.min(stack.width, stack.length)
Expand All @@ -593,13 +599,13 @@
(stack.slotWidth ?? stack.count * stack.thickness) / stack.count}
{@const posX = trayXOffset + stack.x + (counterIdx + 0.5) * counterSpacing}
{@const posZ = trayZOffset - stack.y - (stack.slotDepth ?? stack.length) / 2}
{#if stack.shape === 'square' || stack.shape === 'custom'}
{#if effectiveShape === 'square' || effectiveShape === 'rectangle'}
<!-- Standing on edge: thickness along X (stacking), height along Y, length along Z -->
<T.Mesh position.x={posX} position.y={counterY} position.z={posZ}>
<T.BoxGeometry args={[stack.thickness, standingHeight, stack.length]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else if stack.shape === 'circle'}
{:else if effectiveShape === 'circle'}
<!-- Cylinder standing on edge: rotate so axis is along X -->
<T.Mesh
position.x={posX}
Expand Down Expand Up @@ -631,12 +637,12 @@
(stack.slotDepth ?? stack.count * stack.thickness) / stack.count}
{@const posX = trayXOffset + stack.x + (stack.slotWidth ?? stack.length) / 2}
{@const posZ = trayZOffset - stack.y - (counterIdx + 0.5) * counterSpacing}
{#if stack.shape === 'square' || stack.shape === 'custom'}
{#if effectiveShape === 'square' || effectiveShape === 'rectangle'}
<T.Mesh position.x={posX} position.y={counterY} position.z={posZ}>
<T.BoxGeometry args={[stack.length, standingHeight, stack.thickness]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else if stack.shape === 'circle'}
{:else if effectiveShape === 'circle'}
<T.Mesh
position.x={posX}
position.y={counterY}
Expand Down Expand Up @@ -673,12 +679,14 @@
{@const counterColor = isAlt
? `hsl(${(stackIdx * 137.508) % 360}, 50%, 40%)`
: stack.color}
{#if stack.shape === 'square' || stack.shape === 'custom'}
{@const effectiveShape =
stack.shape === 'custom' ? (stack.customBaseShape ?? 'rectangle') : stack.shape}
{#if effectiveShape === 'square' || effectiveShape === 'rectangle'}
<T.Mesh position.x={posX} position.y={posY} position.z={posZ}>
<T.BoxGeometry args={[stack.width, stack.thickness, stack.length]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else if stack.shape === 'circle'}
{:else if effectiveShape === 'circle'}
<T.Mesh position.x={posX} position.y={posY} position.z={posZ}>
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 32]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
Expand Down
Loading