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
37 changes: 36 additions & 1 deletion src/lib/components/GlobalsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
{ value: 'rectangle', label: 'Rectangle' },
{ value: 'square', label: 'Square' },
{ value: 'circle', label: 'Circle' },
{ value: 'hex', label: 'Hex' }
{ value: 'hex', label: 'Hex' },
{ value: 'triangle', label: 'Triangle' }
];

function updateParam<K extends keyof CounterTrayParams>(key: K, value: CounterTrayParams[K]) {
Expand Down Expand Up @@ -158,6 +159,27 @@
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">Triangle (side)</span>
<input
type="number"
step="0.1"
value={params.triangleSide}
onchange={(e) => updateParam('triangleSide', 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">Triangle Radius</span>
<input
type="number"
step="0.1"
min="0"
value={params.triangleCornerRadius}
onchange={(e) => updateParam('triangleCornerRadius', 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">Thickness</span>
<input
Expand Down Expand Up @@ -283,6 +305,19 @@
class="mt-1 block w-full rounded border-gray-600 bg-gray-700 px-2 py-1 text-sm"
/>
</label>
{:else if baseShape === 'triangle'}
<label class="col-span-2 block">
<span class="text-xs text-gray-400">Side</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>
Expand Down
86 changes: 74 additions & 12 deletions src/lib/components/TrayScene.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -448,9 +448,11 @@
{@const effectiveShape =
stack.shape === 'custom' ? (stack.customBaseShape ?? 'rectangle') : stack.shape}
{@const standingHeight =
stack.shape === 'custom'
? Math.min(stack.width, stack.length)
: Math.max(stack.width, stack.length)}
effectiveShape === 'triangle'
? stack.length // Triangle geometric height (point down)
: stack.shape === 'custom'
? Math.min(stack.width, stack.length)
: Math.max(stack.width, stack.length)}
{@const counterY = stack.z + standingHeight / 2}
{@const isAlt = counterIdx % 2 === 1}
{@const counterColor = isAlt ? `hsl(${(stackIdx * 137.508) % 360}, 50%, 40%)` : stack.color}
Expand All @@ -476,7 +478,7 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 32]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
{:else if effectiveShape === 'hex'}
<!-- hex: rotate so axis is along X -->
<T.Mesh
position.x={posX}
Expand All @@ -488,6 +490,18 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 6]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
<!-- triangle: rotate so axis is along X -->
<T.Mesh
position.x={posX}
position.y={counterY}
position.z={posZ}
rotation.z={Math.PI / 2}
rotation.x={Math.PI / 2}
>
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 3]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{/if}
{:else}
<!-- Crosswise: counters arranged along Y axis (Z in Three.js) -->
Expand All @@ -509,7 +523,7 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 32]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
{:else if effectiveShape === 'hex'}
<!-- hex -->
<T.Mesh
position.x={posX}
Expand All @@ -521,6 +535,17 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 6]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
<!-- triangle -->
<T.Mesh
position.x={posX}
position.y={counterY}
position.z={posZ}
rotation.x={Math.PI / 2}
>
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 3]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{/if}
{/if}
{/each}
Expand All @@ -545,7 +570,7 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 32]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
{:else if effectiveShape === 'hex'}
<!-- hex -->
<T.Mesh
position.x={posX}
Expand All @@ -556,6 +581,12 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 6]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
<!-- triangle -->
<T.Mesh position.x={posX} position.y={posY} position.z={posZ}>
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 3]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{/if}
{/each}
{/if}
Expand Down Expand Up @@ -586,9 +617,11 @@
{@const effectiveShape =
stack.shape === 'custom' ? (stack.customBaseShape ?? 'rectangle') : stack.shape}
{@const standingHeight =
stack.shape === 'custom'
? Math.min(stack.width, stack.length)
: Math.max(stack.width, stack.length)}
effectiveShape === 'triangle'
? stack.length // Triangle geometric height (point down)
: stack.shape === 'custom'
? Math.min(stack.width, stack.length)
: Math.max(stack.width, stack.length)}
{@const counterY = trayYOffset + stack.z + standingHeight / 2}
{@const isAlt = counterIdx % 2 === 1}
{@const counterColor = isAlt
Expand Down Expand Up @@ -618,7 +651,7 @@
/>
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
{:else if effectiveShape === 'hex'}
<!-- hex: rotate so axis is along X -->
<T.Mesh
position.x={posX}
Expand All @@ -630,6 +663,18 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 6]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
<!-- triangle: rotate so axis is along X -->
<T.Mesh
position.x={posX}
position.y={counterY}
position.z={posZ}
rotation.z={Math.PI / 2}
rotation.x={Math.PI / 2}
>
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 3]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{/if}
{:else}
<!-- Crosswise: counters arranged along Y axis (Z in Three.js) -->
Expand All @@ -654,7 +699,7 @@
/>
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
{:else if effectiveShape === 'hex'}
<T.Mesh
position.x={posX}
position.y={counterY}
Expand All @@ -665,6 +710,17 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 6]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
<!-- triangle -->
<T.Mesh
position.x={posX}
position.y={counterY}
position.z={posZ}
rotation.x={Math.PI / 2}
>
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 3]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{/if}
{/if}
{/each}
Expand All @@ -691,7 +747,7 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 32]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
{:else if effectiveShape === 'hex'}
<!-- hex -->
<T.Mesh
position.x={posX}
Expand All @@ -702,6 +758,12 @@
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 6]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{:else}
<!-- triangle -->
<T.Mesh position.x={posX} position.y={posY} position.z={posZ}>
<T.CylinderGeometry args={[stack.width / 2, stack.width / 2, stack.thickness, 3]} />
<T.MeshStandardMaterial color={counterColor} roughness={0.4} metalness={0.2} />
</T.Mesh>
{/if}
{/each}
{/if}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/TraysPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
handleDragEnd();
}

const builtinShapes = ['square', 'hex', 'circle'] as const;
const builtinShapes = ['square', 'hex', 'circle', 'triangle'] as const;
let shapeOptions = $derived([
...builtinShapes,
...(selectedTray?.params.customShapes.map((s) => `custom:${s.name}`) ?? [])
Expand Down
17 changes: 17 additions & 0 deletions src/lib/models/box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function getTrayDimensions(params: CounterTrayParams): TrayDimensions {
squareLength,
hexFlatToFlat: hexFlatToFlatBase,
circleDiameter: circleDiameterBase,
triangleSide: triangleSideBase,
counterThickness,
hexPointyTop,
clearance,
Expand Down Expand Up @@ -74,6 +75,12 @@ export function getTrayDimensions(params: CounterTrayParams): TrayDimensions {
const circlePocketWidth = circleDiameter;
const circlePocketLength = circleDiameter;

// Triangle: equilateral, side length is the base (X), height is side * sqrt(3)/2 (Y)
const triangleSide = triangleSideBase + clearance * 2;
const triangleHeight = triangleSide * (Math.sqrt(3) / 2);
const trianglePocketWidth = triangleSide;
const trianglePocketLength = triangleHeight;

// Helper to get custom shape by name from 'custom:ShapeName' reference
const getCustomShape = (shapeRef: string) => {
if (!shapeRef.startsWith('custom:')) return null;
Expand All @@ -96,6 +103,12 @@ export function getTrayDimensions(params: CounterTrayParams): TrayDimensions {
const l = hexPointyTop ? pointToPoint : flatToFlat;
return [w, l];
}
if (baseShape === 'triangle') {
// width stores side length, height = side * sqrt(3)/2
const side = custom.width;
const height = side * (Math.sqrt(3) / 2);
return [side, height]; // Base (X) x Height (Y)
}
if (baseShape === 'circle' || baseShape === 'square') {
// Both dimensions are equal (diameter or size)
return [custom.width, custom.width];
Expand All @@ -108,6 +121,7 @@ export function getTrayDimensions(params: CounterTrayParams): TrayDimensions {
const getPocketWidth = (shape: string): number => {
if (shape === 'square') return squarePocketWidth;
if (shape === 'hex') return hexPocketWidth;
if (shape === 'triangle') return trianglePocketWidth;
const custom = getCustomShape(shape);
if (custom) {
const [w, l] = getCustomEffectiveDims(custom);
Expand All @@ -119,6 +133,7 @@ export function getTrayDimensions(params: CounterTrayParams): TrayDimensions {
const getPocketLength = (shape: string): number => {
if (shape === 'square') return squarePocketLength;
if (shape === 'hex') return hexPocketLength;
if (shape === 'triangle') return trianglePocketLength;
const custom = getCustomShape(shape);
if (custom) {
const [w, l] = getCustomEffectiveDims(custom);
Expand Down Expand Up @@ -163,6 +178,7 @@ export function getTrayDimensions(params: CounterTrayParams): TrayDimensions {
if (shape === 'square') return squareWidth;
if (shape === 'hex')
return hexPointyTop ? hexFlatToFlatBase : hexFlatToFlatBase / Math.cos(Math.PI / 6);
if (shape === 'triangle') return triangleSideBase;
const custom = getCustomShape(shape);
if (custom) {
const [w, l] = getCustomEffectiveDims(custom);
Expand All @@ -175,6 +191,7 @@ export function getTrayDimensions(params: CounterTrayParams): TrayDimensions {
if (shape === 'square') return squareLength;
if (shape === 'hex')
return hexPointyTop ? hexFlatToFlatBase / Math.cos(Math.PI / 6) : hexFlatToFlatBase;
if (shape === 'triangle') return triangleSideBase * (Math.sqrt(3) / 2);
const custom = getCustomShape(shape);
if (custom) {
const [w, l] = getCustomEffectiveDims(custom);
Expand Down
Loading