From 8c61cd182ec1911280fb1e3d5236fdd41294ad83 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 16 Apr 2026 14:59:04 +0300 Subject: [PATCH] feat(furniture): add scratching post with simple, tree, condo, wall variants New SCRATCHING_POST type under a new "pets" category with 8 presets covering single posts, multi-tier cat trees, enclosed condos, and wall-mounted scratcher pads. Includes 3D meshes, i18n labels (en/ru), and palette integration. --- .../client/public/locales/en/translation.json | 1 + .../client/public/locales/ru/translation.json | 1 + .../editor/panels/FurniturePalette.tsx | 1 + .../editor/symbols/furniture/index.ts | 14 ++ .../components/editor/three/FurnitureMesh.tsx | 147 ++++++++++++++++++ packages/shared/src/types/elements.ts | 1 + 6 files changed, 165 insertions(+) diff --git a/apps/client/public/locales/en/translation.json b/apps/client/public/locales/en/translation.json index fc0c866..3cb803b 100644 --- a/apps/client/public/locales/en/translation.json +++ b/apps/client/public/locales/en/translation.json @@ -323,6 +323,7 @@ "furnitureCategory.electronics": "Electronics", "furnitureCategory.climate": "Climate", "furnitureCategory.decor": "Decor", + "furnitureCategory.pets": "Pets", "cableLength.label": "Cable length:", diff --git a/apps/client/public/locales/ru/translation.json b/apps/client/public/locales/ru/translation.json index d4d0afb..6c0e499 100644 --- a/apps/client/public/locales/ru/translation.json +++ b/apps/client/public/locales/ru/translation.json @@ -326,6 +326,7 @@ "furnitureCategory.electronics": "Электроника", "furnitureCategory.climate": "Климат", "furnitureCategory.decor": "Декор", + "furnitureCategory.pets": "Питомцы", "cableLength.label": "Длина кабеля:", diff --git a/apps/client/src/components/editor/panels/FurniturePalette.tsx b/apps/client/src/components/editor/panels/FurniturePalette.tsx index af4c1d2..7c8a3f2 100644 --- a/apps/client/src/components/editor/panels/FurniturePalette.tsx +++ b/apps/client/src/components/editor/panels/FurniturePalette.tsx @@ -35,6 +35,7 @@ const CATEGORY_META: Record = electronics: { icon: '\u{1F4FA}', key: 'furnitureCategory.electronics' }, climate: { icon: '\u{1F525}', key: 'furnitureCategory.climate' }, decor: { icon: '\u{1F5BC}', key: 'furnitureCategory.decor' }, + pets: { icon: '\u{1F431}', key: 'furnitureCategory.pets' }, }; type CategoryFilter = FurnitureCategory | 'all'; diff --git a/apps/client/src/components/editor/symbols/furniture/index.ts b/apps/client/src/components/editor/symbols/furniture/index.ts index 8d9cc1a..128638a 100644 --- a/apps/client/src/components/editor/symbols/furniture/index.ts +++ b/apps/client/src/components/editor/symbols/furniture/index.ts @@ -22,6 +22,7 @@ export const FURNITURE_CATEGORIES = [ 'electronics', 'climate', 'decor', + 'pets', ] as const; export type FurnitureCategory = (typeof FURNITURE_CATEGORIES)[number]; @@ -164,4 +165,17 @@ export const FURNITURE_DEFS: readonly FurnitureDef[] = [ { type: 'MIRROR', category: 'decor', label: 'Wall Mirror (L)', width: 1.0, depth: 0.04, height: 1.5, icon: '\u{1FA9E}', defaultMetadata: { variant: 'wall' } }, { type: 'MIRROR', category: 'decor', label: 'Floor Mirror', width: 0.5, depth: 0.4, height: 1.6, icon: '\u{1FA9E}', defaultMetadata: { variant: 'floor' } }, { type: 'MIRROR', category: 'decor', label: 'Floor Mirror (L)', width: 0.6, depth: 0.5, height: 1.8, icon: '\u{1FA9E}', defaultMetadata: { variant: 'floor' } }, + // Scratching posts — cat furniture. Variants control the mesh style: + // - `simple` → single sisal-wrapped post on a base with a small platform on top + // - `tree` → multi-tier cat tree with 2–3 platforms at different heights + // - `condo` → tower with an enclosed cubby and a platform on the roof + // - `wall` → flat wall-mounted scratcher pad (thin, meant to be elevated) + { type: 'SCRATCHING_POST', category: 'pets', label: 'Scratching Post (S)', width: 0.3, depth: 0.3, height: 0.5, icon: '\u{1F431}', defaultMetadata: { variant: 'simple' } }, + { type: 'SCRATCHING_POST', category: 'pets', label: 'Scratching Post', width: 0.35, depth: 0.35, height: 0.7, icon: '\u{1F431}', defaultMetadata: { variant: 'simple' } }, + { type: 'SCRATCHING_POST', category: 'pets', label: 'Cat Tree (S)', width: 0.45, depth: 0.45, height: 0.9, icon: '\u{1F408}', defaultMetadata: { variant: 'tree' } }, + { type: 'SCRATCHING_POST', category: 'pets', label: 'Cat Tree (M)', width: 0.55, depth: 0.55, height: 1.3, icon: '\u{1F408}', defaultMetadata: { variant: 'tree' } }, + { type: 'SCRATCHING_POST', category: 'pets', label: 'Cat Tree (L)', width: 0.65, depth: 0.65, height: 1.7, icon: '\u{1F408}', defaultMetadata: { variant: 'tree' } }, + { type: 'SCRATCHING_POST', category: 'pets', label: 'Cat Condo', width: 0.45, depth: 0.45, height: 1.0, icon: '\u{1F3E0}', defaultMetadata: { variant: 'condo' } }, + { type: 'SCRATCHING_POST', category: 'pets', label: 'Cat Condo (L)', width: 0.55, depth: 0.55, height: 1.4, icon: '\u{1F3E0}', defaultMetadata: { variant: 'condo' } }, + { type: 'SCRATCHING_POST', category: 'pets', label: 'Wall Scratcher', width: 0.2, depth: 0.04, height: 0.6, icon: '\u{1F431}', defaultMetadata: { variant: 'wall' } }, ]; diff --git a/apps/client/src/components/editor/three/FurnitureMesh.tsx b/apps/client/src/components/editor/three/FurnitureMesh.tsx index f7b6047..fc84e45 100644 --- a/apps/client/src/components/editor/three/FurnitureMesh.tsx +++ b/apps/client/src/components/editor/three/FurnitureMesh.tsx @@ -46,6 +46,7 @@ const FURNITURE_COLORS: Record = { MIRROR: '#8a7a5c', DIGITAL_PIANO: '#1a1a22', SPEAKER: '#20201e', + SCRATCHING_POST: '#c8a96e', OTHER: '#a0a0a0', }; @@ -1615,6 +1616,151 @@ function OfficeChairMesh({ item, color }: { readonly item: FurnitureItem; readon ); } +// ── Scratching Post ────────────────────────────────────────────── +// +// Variants: +// simple → single sisal-wrapped post with a round platform on top +// tree → multi-tier cat tree (2–3 posts + round platforms at offsets) +// condo → enclosed cubby box with a platform on top and a post +// wall → flat wall-mounted scratcher pad + +const SISAL_COLOR = '#c8a96e'; +const PLATFORM_COLOR = '#8a7560'; +const CONDO_COLOR = '#7a6a5a'; + +function ScratchingPostMesh({ item, color }: { readonly item: FurnitureItem; readonly color: string }) { + const variant = (item.metadata as Record | null)?.variant as string | undefined ?? 'simple'; + const sisalMat = useMemo(() => getFurnitureMaterial(color, 0.85), [color]); + const platformMat = useMemo(() => getFurnitureMaterial(PLATFORM_COLOR, 0.7), []); + const condoMat = useMemo(() => getFurnitureMaterial(CONDO_COLOR, 0.6), []); + + const w = item.width; + const d = item.depth; + const h = item.height; + const postRadius = Math.min(w, d) * 0.12; + const platformRadius = Math.min(w, d) * 0.4; + const platformThickness = 0.025; + + if (variant === 'wall') { + // Flat scratcher pad + const padThickness = d > 0.01 ? d : 0.03; + return ( + + + + + + + + + + ); + } + + if (variant === 'condo') { + const cubbyH = h * 0.45; + const cubbyW = w * 0.8; + const cubbyD = d * 0.8; + const wallThick = 0.02; + const postH = h - cubbyH - platformThickness; + const openingRadius = Math.min(cubbyW, cubbyD) * 0.35; + + return ( + + {/* Base platform */} + + + + {/* Cubby box */} + + + + {/* Cubby opening (dark circle on front face) */} + + + + + {/* Post on top of cubby */} + + + + {/* Top platform */} + + + + + ); + } + + if (variant === 'tree') { + // Multi-tier cat tree: 2–3 posts at offsets with platforms + const tierCount = h > 1.2 ? 3 : 2; + const baseR = platformRadius * 1.1; + const offsets = [ + { x: -w * 0.18, z: -d * 0.15, ph: h * 0.4, pr: platformRadius * 0.85 }, + { x: w * 0.15, z: d * 0.12, ph: h * 0.7, pr: platformRadius * 0.9 }, + { x: -w * 0.05, z: d * 0.05, ph: h, pr: platformRadius }, + ].slice(0, tierCount); + + return ( + + {/* Base */} + + + + {offsets.map((tier, i) => ( + + {/* Post */} + + + + {/* Platform */} + + + + + ))} + + ); + } + + // Default: simple — single post on a base with a platform on top + return ( + + {/* Base */} + + + + {/* Post */} + + + + {/* Top platform */} + + + + + ); +} + function getFurnitureComponent(type: FurnitureType) { switch (type) { case 'BED': return BedMesh; @@ -1637,6 +1783,7 @@ function getFurnitureComponent(type: FurnitureType) { case 'MIRROR': return MirrorMesh; case 'DIGITAL_PIANO': return DigitalPianoMesh; case 'SPEAKER': return SpeakerMesh; + case 'SCRATCHING_POST': return ScratchingPostMesh; case 'NIGHTSTAND': return NightstandMesh; case 'SHELF': case 'DRESSER': diff --git a/packages/shared/src/types/elements.ts b/packages/shared/src/types/elements.ts index cd85301..feb34d2 100644 --- a/packages/shared/src/types/elements.ts +++ b/packages/shared/src/types/elements.ts @@ -348,6 +348,7 @@ export const FURNITURE_TYPES = [ 'MIRROR', 'DIGITAL_PIANO', 'SPEAKER', + 'SCRATCHING_POST', 'OTHER', ] as const; export type FurnitureType = (typeof FURNITURE_TYPES)[number];