declare var QRCodeStyling: any; document.addEventListener('DOMContentLoaded', () => { // --- DATA --- const sampleProducts = [ { id: "shisa_demo", name: "【ARDemo】チェア", price: "¥10,000", description: "ARDemo用のチェア。", modelSrc: "models/Demo0001.glb", altText: "チェアの3Dモデル", environmentImage: "environments/brown_photostudio_01_1k.jpg", exposure: "1.0", hotspots: [ { slot: "hotspot-shisa-depth", position: "0.2275 0 -0.2325", normal: "0 0 1", text: "奥行: 45.5cm" }, { slot: "hotspot-shisa-width", position: "0.2275 0 0.2325", normal: "1 0 0", text: "横幅: 46.5cm" }, { slot: "hotspot-shisa-height", position: "0 0.83 -0.2325", normal: "0 1 0", text: "高さ:83cm" } ] }, { id: "tina_audio", name: "TINA AUDIO 1.0", price: "¥385,000", description: "TINA AUDIO 1.0(直径10cm高さ1m)", modelSrc: "models/tina_audio_1.glb", altText: "TINA AUDIO 1.0の3Dモデル", environmentImage: "environments/brown_photostudio_02_1k.hdr", exposure: "1.0", hotspots: [ { slot: "hotspot-tina-height", position: "0 0.984 0.057", normal: "0 0 1", text: "高さ(Height): 984mm" }, { slot: "hotspot-tina-diameter", position: "0.0575 0.5 0", normal: "1 0 0", text: "直径(Ø): 115mm" }, { slot: "hotspot-tina-basediameter", position: "0 0.01 0.11", normal: "0 0 1", text: "底板直径(Bottom Ø): 220mm" }, { slot: "hotspot-tina-topdiameter", position: "0 0.984 -0.057", normal: "0 0 -1", text: "天板直径(Top Ø): 114mm" } ] }, { id: "tina_audio_1_5", name: "TINA AUDIO 1.5", price: "¥517,000", description: "TINA AUDIO 1.5(直径10cm高さ1.5m)", modelSrc: "models/tina_audio_1_5.glb", altText: "TINA AUDIO 1.5の3Dモデル", environmentImage: "environments/brown_photostudio_02_1k.hdr", exposure: "1.0", hotspots: [ { slot: "hotspot-tina1.5-height", position: "0 1.457 0.057", normal: "0 0 1", text: "高さ(Height): 1457mm" }, { slot: "hotspot-tina1.5-diameter", position: "0.0575 0.75 0", normal: "1 0 0", text: "直径(Ø): 115mm" }, { slot: "hotspot-tina1.5-basediameter", position: "0 0.01 0.12", normal: "0 0 1", text: "底板直径(Bottom Ø): 240mm" }, { slot: "hotspot-tina1.5-topdiameter", position: "0 1.457 -0.057", normal: "0 0 -1", text: "天板直径(Top Ø): 114mm" } ] }, { id: "tina_audio_2", name: "TINA AUDIO 2.0", price: "¥715,000", description: "TINA AUDIO 2.0(直径10cm高さ2m)", modelSrc: "models/tina_audio_2.glb", altText: "TINA AUDIO 2.0の3Dモデル", environmentImage: "environments/brown_photostudio_02_1k.hdr", exposure: "1.0", hotspots: [ { slot: "hotspot-tina2.0-height", position: "0 1.945 0.115", normal: "0 0 1", text: "高さ(Height): 1945mm" }, { slot: "hotspot-tina2.0-diameter", position: "0.0575 1 0", normal: "1 0 0", text: "直径(Ø): 115mm" }, { slot: "hotspot-tina2.0-basediameter", position: "0 0.01 0.14", normal: "0 0 1", text: "底板直径(Bottom Ø): 280mm" }, { slot: "hotspot-tina2.0-topdiameter", position: "0 1.945 -0.115", normal: "0 0 -1", text: "天板直径(Top Ø): 230mm" } ] }, { id: "tina_audio_2_high_class", name: "TINA AUDIO 2.0 HIGH CLASS", price: "¥1,430,000", description: "TINA AUDIO 2.0(直径20cm高さ2m)", modelSrc: "models/tina_audio_2_HC.glb", altText: "TINA AUDIO 2.0 HIGH CLASSの3Dモデル", environmentImage: "environments/brown_photostudio_02_1k.hdr", exposure: "1.0", hotspots: [ { slot: "hotspot-tina2-height", position: "0 2.03 0.21", normal: "0 0 1", text: "高さ(Height): 2030mm" }, { slot: "hotspot-tina2-diameter", position: "0.1075 1 0", normal: "1 0 0", text: "直径(Ø): 215mm" }, { slot: "hotspot-tina2hc-basediameter", position: "0 0.01 0.175", normal: "0 0 1", text: "底板直径(Bottom Ø): 350mm" }, { slot: "hotspot-tina2hc-topdiameter", position: "0 2.03 -0.21", normal: "0 0 -1", text: "天板直径(Top Ø): 420mm" } ] }, { id: "kerberos_2025", name: "ケルベロス2025", price: "¥77,000", description: "[サイズ]約 幅12cm×高さ約10cm×奥行12㎝ 重量約170g [素 材] 陶器", modelSrc: "models/ikutouen_0001_Kerberos.glb", altText: "ケルベロス2025の3Dモデル", environmentImage: "environments/brown_photostudio_01_1k.jpg", exposure: "1.0", variants: [ { name: "デフォルト", modelSrc: "models/ikutouen_0001_Kerberos.glb", icon: "models/ikutouen/ikutouen_0001_Kerberos.jpg" }, { name: "ピンク", modelSrc: "models/ikutouen_0001_Kerberos_pink.glb", icon: "models/ikutouen/ikutouen_0001_Kerberos_pink.jpg" }, { name: "黄色", modelSrc: "models/ikutouen_0001_Kerberos_yellow.glb", icon: "models/ikutouen/ikutouen_0001_Kerberos_yellow.jpg" }, ], hotspots: [ { slot: "hotspot-kerberos-width", position: "0.06 0.07 0.02", normal: "1 0 0", text: "幅12cm" }, { slot: "hotspot-kerberos-height", position: "0 0.09 0.04", normal: "0 1 0", text: "高さ約10cm" }, { slot: "hotspot-kerberos-depth", position: "0 0 -0.06", normal: "0 0 -1", text: "奥行12㎝" }, { slot: "hotspot-kerberos-weight", position: "0 0 0.06", normal: "0 0 1", text: "重量約170g" } ] }, { id: "large_flower_vase", name: "花器大", price: "¥33,000", description: "[バリエーション] 2色 白唐草線彫//黒釉唐草線彫 [サイズ] 口径約10.5cm× 高さ約25cm 重量約1300g [素材]陶器", modelSrc: "models/ikutouen_0002_Kaki.glb", altText: "花器大の3Dモデル", environmentImage: "environments/brown_photostudio_01_1k.jpg", exposure: "1.0", hotspots: [ { slot: "hotspot-vase-diameter", position: "0 0.24 0.0525", normal: "0 0 1", text: "口径約10.5cm" }, { slot: "hotspot-vase-height", position: "0 0.24 0", normal: "0 1 0", text: "高さ約25cm" }, { slot: "hotspot-vase-weight", position: "0 0 0.0425", normal: "0 0 1", text: "重量約1300g" } ] }, { id: "shisa_17cm_milky_white", name: "横向17cm 透かし彫り獅子 乳白", price: "¥264,000", description: "[サイズ] 幅約15cm×高さ約17cm×奥行19㎝ 重量約1.2kg (1匹あたり) [素材] 陶器", modelSrc: "models/ikutouen_0003_ShisaLR.glb", altText: "横向17cm透かし彫り獅子乳白の3Dモデル", environmentImage: "environments/brown_photostudio_01_1k.jpg", exposure: "1.0" } ]; // --- DOM ELEMENTS --- const appContainer = document.getElementById('app-container'); const viewerContainer = document.getElementById('viewer-container'); const contentContainer = document.getElementById('content-container'); const footer = document.getElementById('footer'); const modelViewer = document.getElementById('model-viewer') as any; const arButton = document.getElementById('ar-button'); const productSelector = document.getElementById('product-selector') as HTMLSelectElement; const productNameEl = document.getElementById('product-name'); const productPriceEl = document.getElementById('product-price'); const productDescriptionEl = document.getElementById('product-description'); const dimensionToggle = document.getElementById('dimension-toggle'); const dimensionToggleText = document.getElementById('dimension-toggle-text'); const dimensionToggleContainer = document.getElementById('dimension-toggle-container'); const variantControls = document.getElementById('variant-controls'); const qrCodeContainer = document.getElementById('qrcode-container'); const qrCodeProductName = document.getElementById('qrcode-product-name'); const qrCodeDownloadLink = document.getElementById('qrcode-download-link'); const qrCodeColorPicker = document.getElementById('qrcode-color-picker') as HTMLInputElement; // New QR Elements const qrTabs = document.querySelectorAll('.qr-tab'); const qrPanels = document.querySelectorAll('.qr-panel'); const qrTextInput = document.getElementById('qr-text') as HTMLInputElement; const qrTextApplyBtn = document.getElementById('qr-text-apply'); const qrTextColorHexInput = document.getElementById('qr-text-color-hex') as HTMLInputElement; const qrTextColorPicker = document.getElementById('qr-text-color') as HTMLInputElement; const qrImageUpload = document.getElementById('qr-image-upload') as HTMLInputElement; const qrImagePreviewContainer = document.getElementById('qr-image-preview-container'); const qrImagePreview = document.getElementById('qr-image-preview') as HTMLImageElement; const qrImageRemoveBtn = document.getElementById('qr-image-remove'); // --- STATE --- let showDimensions = true; let isModelOnlyView = false; let currentDisplayUrl = ''; let currentProductName = ''; let qrCodeInstance = null; let qrOverlay = { type: 'text', text: '', textColor: '#000000', image: null, }; // --- FUNCTIONS --- const escapeXml = (unsafe) => { return unsafe.replace(/[<>&'"]/g, c => { switch (c) { case '<': return '<'; case '>': return '>'; case '&': return '&'; case '\'': return '''; case '"': return '"'; default: return c; } }); }; const getQrTextFontSize = (length) => { switch (length) { case 1: return 48; case 2: return 36; case 3: return 28; case 4: return 22; case 5: return 22; default: return 22; } }; const updateQRCode = () => { qrCodeContainer.innerHTML = ''; if (!currentDisplayUrl) return; const color = qrCodeColorPicker.value; const dotShape = (document.querySelector('input[name="dot-shape"]:checked') as HTMLInputElement).value; const dotsOptions: any = { color: color, type: dotShape }; const cornersSquareOptions: any = { color: color }; const cornersDotOptions: any = { color: color }; switch (dotShape) { case 'rounded': cornersSquareOptions.type = 'extra-rounded'; cornersDotOptions.type = 'dot'; break; case 'dots': cornersSquareOptions.type = 'dot'; cornersDotOptions.type = 'dot'; break; default: cornersSquareOptions.type = 'square'; cornersDotOptions.type = 'square'; break; } const options: any = { width: 280, height: 280, data: currentDisplayUrl, dotsOptions: dotsOptions, cornersSquareOptions: cornersSquareOptions, cornersDotOptions: cornersDotOptions, backgroundOptions: { color: "#ffffff" }, qrOptions: { errorCorrectionLevel: 'H' }, imageOptions: { imageSize: 0.4, margin: 4 } }; if (qrOverlay.type === 'image' && qrOverlay.image) { options.image = qrOverlay.image; options.imageOptions.hideBackgroundDots = true; } else if (qrOverlay.type === 'text' && qrOverlay.text) { const sanitizedText = escapeXml(qrOverlay.text); const fontSize = getQrTextFontSize(qrOverlay.text.length); const svg = ``; // Correctly encode SVG with 2-byte characters for btoa options.image = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg))); options.imageOptions.hideBackgroundDots = false; } try { qrCodeInstance = new QRCodeStyling(options); qrCodeInstance.append(qrCodeContainer); qrCodeProductName.textContent = currentProductName; } catch (e) { console.error("QR Code generation failed:", e); } }; const loadProduct = (productId, modelSrc?) => { const product = sampleProducts.find(p => p.id === productId); if (!product) return; // Update viewer attributes const srcToLoad = modelSrc || product.modelSrc; if (modelViewer.src !== srcToLoad) { modelViewer.src = srcToLoad; } modelViewer.alt = product.altText; modelViewer.environmentImage = product.environmentImage || ''; modelViewer.exposure = product.exposure || '1.0'; modelViewer.setAttribute('aria-label', `Interactive 3D model of ${product.name}`); productNameEl.textContent = product.name; productPriceEl.textContent = product.price; productDescriptionEl.textContent = product.description; // Update URL without reloading page const url = new URL(window.location.href); url.searchParams.set('product', productId); history.pushState({}, '', url); // Update hotspots const existingHotspots = modelViewer.querySelectorAll('[slot^="hotspot-"]'); existingHotspots.forEach(h => h.remove()); if (product.hotspots) { product.hotspots.forEach(hotspot => { const button = document.createElement('button'); button.slot = hotspot.slot; button.setAttribute('data-position', hotspot.position); button.setAttribute('data-normal', hotspot.normal); button.innerHTML = `