commit a73efe2a22130ce9307fde1d9696168c794691ee Author: Hermes Date: Wed Aug 5 11:15:07 2026 +0000 🚀 Inicialização Automática do Laboratório WebXR diff --git a/.agents/skills/hz-unity-meta-quest-ui/SKILL.md b/.agents/skills/hz-unity-meta-quest-ui/SKILL.md new file mode 100644 index 0000000..c8ef27d --- /dev/null +++ b/.agents/skills/hz-unity-meta-quest-ui/SKILL.md @@ -0,0 +1,409 @@ +--- +name: hz-unity-meta-quest-ui +license: Apache-2.0 +description: Configures Unity UI for Meta Quest and Horizon OS VR development — world-space canvases, TextMesh Pro setup, comfortable sizing, viewing distances, and interaction readiness. +--- + +# Meta Quest VR UI Setup + +## When to use this skill + +Use this skill automatically when: +- Setting up a Canvas for VR +- Creating UI text with TextMesh Pro in a VR project +- Adding buttons, sliders, or other interactive UI in VR +- User reports pink/magenta text, unclickable buttons, or UI sizing issues in VR +- Configuring VR interaction (ray or poke) on a Canvas + +## Prerequisite: TMP Essential Resources + +Before creating ANY VR UI, verify TMP resources are imported. Use `Unity_RunCommand`: + +```csharp +using UnityEngine; +using UnityEditor; +using System.IO; + +internal class CommandScript : IRunCommand +{ + public void Execute(ExecutionResult result) + { + string fontPath = "Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset"; + var font = AssetDatabase.LoadAssetAtPath(fontPath); + if (font != null) + result.Log("TMP Essential Resources: IMPORTED. Default font present."); + else + result.LogError("TMP Essential Resources: NOT IMPORTED. Use tmp-resources skill first."); + } +} +``` + +If not imported, use the **tmp-resources** skill before proceeding. + +## Step 1: Create World Space Canvas + +Use `Unity_RunCommand` to create and configure the canvas: + +```csharp +using UnityEngine; +using UnityEditor; +using UnityEngine.UI; + +internal class CommandScript : IRunCommand +{ + public void Execute(ExecutionResult result) + { + // Adapt the name to match your canvas (e.g., "MainMenu", "SettingsUI") + var go = new GameObject("MenuUI"); + var canvas = go.AddComponent(); + canvas.renderMode = RenderMode.WorldSpace; + + go.AddComponent(); + + // Remove CanvasScaler — not appropriate for VR + var scaler = go.GetComponent(); + if (scaler != null) + Object.DestroyImmediate(scaler); + + var rt = go.GetComponent(); + rt.localScale = new Vector3(0.001f, 0.001f, 0.001f); + rt.sizeDelta = new Vector2(1920f, 1080f); + rt.position = new Vector3(0f, 1.5f, 2f); + + result.RegisterObjectCreation(go); + result.Log("Created VR Canvas '{0}'. Scale: {1}, Size: {2}, Position: {3}", + go.name, rt.localScale, rt.sizeDelta, rt.position); + } +} +``` + +### Canvas rules + +- **Render Mode**: Always World Space. Screen Space modes break stereo rendering. +- **Scale**: 0.001 on all axes (1 unit in canvas = 1mm in world). +- **CanvasScaler**: Remove it. Physical size is controlled by world scale, not screen adaptation. +- **Distance**: Place 1.5-3m from user. Never closer than 0.5m. Max 5m for readable text. +- **Physical size formula**: `Canvas sizeDelta * scale = meters`. Example: 1920 * 0.001 = 1.92m wide. + +## Step 2: Create child UI elements + +All child elements (panels, buttons, text) must follow these rules: + +- **localScale**: Always `[1, 1, 1]`. Never scale children to compensate for canvas scale. +- **localPosition.z**: Always `0`. Children must sit on the canvas plane. +- **Size control**: Use `RectTransform.sizeDelta` and anchors, never scale. + +```csharp +using UnityEngine; +using UnityEditor; +using UnityEngine.UI; +using TMPro; + +internal class CommandScript : IRunCommand +{ + public void Execute(ExecutionResult result) + { + // Replace "MenuUI" with the actual canvas name used in Step 1 + var canvas = GameObject.Find("MenuUI"); + if (canvas == null) { result.LogError("Canvas 'MenuUI' not found."); return; } + + // Panel + var panel = new GameObject("ButtonPanel"); + panel.transform.SetParent(canvas.transform, false); + var panelRT = panel.AddComponent(); + panelRT.localScale = Vector3.one; + panelRT.sizeDelta = new Vector2(800f, 600f); + var panelImg = panel.AddComponent(); + panelImg.color = new Color(0.1f, 0.1f, 0.1f, 0.95f); + panelImg.raycastTarget = false; + + var layout = panel.AddComponent(); + layout.spacing = 50f; + layout.padding = new RectOffset(80, 80, 100, 100); + layout.childAlignment = TextAnchor.MiddleCenter; + + // Button + var btnGO = new GameObject("StartButton"); + btnGO.transform.SetParent(panel.transform, false); + var btnRT = btnGO.AddComponent(); + btnRT.localScale = Vector3.one; + btnRT.sizeDelta = new Vector2(400f, 120f); + var btnImg = btnGO.AddComponent(); + btnImg.color = new Color(0.2f, 0.6f, 1f, 1f); + btnGO.AddComponent + + +
+ +
+
+ +

Documentation

+

Your questions, answered

+ +
+
+ +

Connect with us

+

Join the Vite community

+ +
+
+ +
+
+ + ) +} + +export default App diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..5fb3313 --- /dev/null +++ b/src/index.css @@ -0,0 +1,111 @@ +:root { + --text: #6b6375; + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow: + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + } + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +#root { + width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +body { + margin: 0; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} diff --git a/src/lib/convertIFC.ts b/src/lib/convertIFC.ts new file mode 100644 index 0000000..bfdb696 --- /dev/null +++ b/src/lib/convertIFC.ts @@ -0,0 +1,278 @@ +import * as THREE from 'three'; +import * as WebIFC from 'web-ifc'; +import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'; + +const WASM_PATH = 'https://unpkg.com/web-ifc@0.0.57/'; + +/** + * Convert an IFC file (ArrayBuffer) into a GLB Blob using web-ifc + GLTFExporter. + */ +function getMaterialName(ifcApi: WebIFC.IfcAPI, modelID: number, matRef: any): string | null { + if (!matRef) return null; + const matId = matRef.value; + if (!matId) return null; + try { + const matLine = ifcApi.GetLine(modelID, matId); + if (!matLine) return null; + + if (matLine.Name && matLine.Name.value) { + return matLine.Name.value; + } + + if (matLine.Materials) { + for (const mRef of matLine.Materials) { + const name = getMaterialName(ifcApi, modelID, mRef); + if (name) return name; + } + } + + if (matLine.MaterialConstituents) { + for (const mcRef of matLine.MaterialConstituents) { + const mcLine = ifcApi.GetLine(modelID, mcRef.value); + if (mcLine && mcLine.Material) { + const name = getMaterialName(ifcApi, modelID, mcLine.Material); + if (name) return name; + } + } + } + + if (matLine.MaterialProfileSet) { + return getMaterialName(ifcApi, modelID, matLine.MaterialProfileSet); + } + if (matLine.MaterialProfiles) { + for (const mpRef of matLine.MaterialProfiles) { + const mpLine = ifcApi.GetLine(modelID, mpRef.value); + if (mpLine && mpLine.Material) { + const name = getMaterialName(ifcApi, modelID, mpLine.Material); + if (name) return name; + } + } + } + } catch (err) { + // Silencia erros de atributos inexistentes + } + return null; +} + +export async function parseIFCtoThree(buffer: ArrayBuffer): Promise { + const ifcApi = new WebIFC.IfcAPI(); + ifcApi.SetWasmPath(WASM_PATH, true); + await ifcApi.Init(); + + const data = new Uint8Array(buffer); + const modelID = ifcApi.OpenModel(data); + + // Mapeamento de materiais + const elementMaterialMap = new Map(); + try { + const rels = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELASSOCIATESMATERIAL); + for (let i = 0; i < rels.size(); i++) { + const relId = rels.get(i); + const rel = ifcApi.GetLine(modelID, relId); + if (rel && rel.RelatedObjects && rel.RelatingMaterial) { + const matName = getMaterialName(ifcApi, modelID, rel.RelatingMaterial); + if (matName) { + for (const objRef of rel.RelatedObjects) { + elementMaterialMap.set(objRef.value, matName); + } + } + } + } + } catch (err) { + console.warn('[IFC Parser] Falha ao mapear materiais:', err); + } + + // Mapeamento de propriedades adicionais + const elementPropertiesMap = new Map>(); + try { + const relsProp = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELDEFINESBYPROPERTIES); + for (let i = 0; i < relsProp.size(); i++) { + const relId = relsProp.get(i); + const rel = ifcApi.GetLine(modelID, relId); + if (rel && rel.RelatedObjects && rel.RelatingPropertyDefinition) { + const propDefId = rel.RelatingPropertyDefinition.value; + const propDef = ifcApi.GetLine(modelID, propDefId); + + if (propDef) { + const props: Record = {}; + + if (propDef.HasProperties) { + for (const pRef of propDef.HasProperties) { + const pLine = ifcApi.GetLine(modelID, pRef.value); + if (pLine && pLine.Name) { + const name = pLine.Name.value; + let value = ''; + if (pLine.NominalValue) { + value = String(pLine.NominalValue.value); + } + if (name && value) { + props[name] = value; + } + } + } + } + + if (propDef.Quantities) { + for (const qRef of propDef.Quantities) { + const qLine = ifcApi.GetLine(modelID, qRef.value); + if (qLine && qLine.Name) { + const name = qLine.Name.value; + let value = ''; + if (qLine.LengthValue !== undefined) value = `${qLine.LengthValue.toFixed(1)} mm`; + else if (qLine.AreaValue !== undefined) value = `${qLine.AreaValue.toFixed(1)} m²`; + else if (qLine.VolumeValue !== undefined) value = `${qLine.VolumeValue.toFixed(2)} m³`; + else if (qLine.NominalValue) value = String(qLine.NominalValue.value); + + if (name && value) { + props[name] = value; + } + } + } + } + + if (Object.keys(props).length > 0) { + for (const objRef of rel.RelatedObjects) { + const existing = elementPropertiesMap.get(objRef.value) ?? {}; + elementPropertiesMap.set(objRef.value, { ...existing, ...props }); + } + } + } + } + } + } catch (err) { + console.warn('[IFC Parser] Falha ao mapear propriedades:', err); + } + + const scene = new THREE.Scene(); + const materials: Map = new Map(); + + ifcApi.StreamAllMeshes(modelID, (mesh: WebIFC.FlatMesh) => { + const placedGeometries = mesh.geometries; + const expressID = (mesh as unknown as { expressID?: number }).expressID ?? 0; + + let name = ''; + let tag = ''; + let objectType = ''; + let description = ''; + try { + const elementLine = ifcApi.GetLine(modelID, expressID); + if (elementLine) { + name = elementLine.Name?.value ?? ''; + tag = elementLine.Tag?.value ?? ''; + objectType = elementLine.ObjectType?.value ?? ''; + description = elementLine.Description?.value ?? ''; + } + } catch (e) {} + + const materialName = elementMaterialMap.get(expressID) ?? ''; + const extraProps = elementPropertiesMap.get(expressID) ?? {}; + + const elementGroup = new THREE.Group(); + elementGroup.name = `ifc_${expressID}`; + elementGroup.userData = { + ifcElement: true, + ifcId: expressID, + materialName, + properties: { + name, + tag, + objectType, + description, + material: materialName, + ...extraProps + } + }; + + for (let i = 0; i < placedGeometries.size(); i++) { + const placedGeometry = placedGeometries.get(i); + const ifcGeometry = ifcApi.GetGeometry(modelID, placedGeometry.geometryExpressID); + + const verts = ifcApi.GetVertexArray( + ifcGeometry.GetVertexData(), + ifcGeometry.GetVertexDataSize() + ); + const indices = ifcApi.GetIndexArray( + ifcGeometry.GetIndexData(), + ifcGeometry.GetIndexDataSize() + ); + + const geometry = new THREE.BufferGeometry(); + const positionArray = new Float32Array(verts.length / 2); + const normalArray = new Float32Array(verts.length / 2); + + for (let j = 0; j < verts.length; j += 6) { + const idx = j / 6; + positionArray[idx * 3] = verts[j]; + positionArray[idx * 3 + 1] = verts[j + 1]; + positionArray[idx * 3 + 2] = verts[j + 2]; + normalArray[idx * 3] = verts[j + 3]; + normalArray[idx * 3 + 1] = verts[j + 4]; + normalArray[idx * 3 + 2] = verts[j + 5]; + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3)); + geometry.setAttribute('normal', new THREE.BufferAttribute(normalArray, 3)); + geometry.setIndex(new THREE.BufferAttribute(indices, 1)); + + const color = placedGeometry.color; + const colorKey = (color.x * 255) << 16 | (color.y * 255) << 8 | (color.z * 255); + let material = materials.get(colorKey); + if (!material) { + material = new THREE.MeshStandardMaterial({ + color: new THREE.Color(color.x, color.y, color.z), + metalness: 0.2, + roughness: 0.8, + transparent: color.w < 1, + opacity: color.w, + side: THREE.DoubleSide, + }); + materials.set(colorKey, material); + } + + const mesh3 = new THREE.Mesh(geometry, material); + mesh3.userData = { ifcId: expressID }; + + const matrix = new THREE.Matrix4(); + matrix.fromArray(placedGeometry.flatTransformation); + mesh3.applyMatrix4(matrix); + + elementGroup.add(mesh3); + ifcGeometry.delete(); + } + + if (elementGroup.children.length > 0) scene.add(elementGroup); + }); + + ifcApi.CloseModel(modelID); + return scene; +} + +export async function convertIFCtoGLB( + buffer: ArrayBuffer, + fileName: string +): Promise<{ blob: Blob; fileName: string; fileSize: number }> { + const scene = await parseIFCtoThree(buffer); + + // Export to GLB + const exporter = new GLTFExporter(); + const glb = await new Promise((resolve, reject) => { + exporter.parse( + scene, + (result) => resolve(result as ArrayBuffer), + (error) => reject(error), + { binary: true } + ); + }); + + const glbFileName = fileName.replace(/\.ifc$/i, '.glb'); + const blob = new Blob([glb], { type: 'model/gltf-binary' }); + + // Cleanup + scene.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.geometry.dispose(); + } + }); + + return { blob, fileName: glbFileName, fileSize: blob.size }; +} diff --git a/src/lib/convertToGLB.ts b/src/lib/convertToGLB.ts new file mode 100644 index 0000000..0af6522 --- /dev/null +++ b/src/lib/convertToGLB.ts @@ -0,0 +1,67 @@ +import * as THREE from 'three'; +import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; +import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; +import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'; + +type SupportedExt = 'glb' | 'obj' | 'stl' | 'ifc'; + +export function getSupportedExtension(fileName: string): SupportedExt | null { + const ext = fileName.split('.').pop()?.toLowerCase(); + if (ext === 'glb' || ext === 'obj' || ext === 'stl' || ext === 'ifc') return ext; + return null; +} + +export const ACCEPTED_EXTENSIONS = '.glb,.obj,.stl,.ifc'; + +/** + * Convert an OBJ or STL file (ArrayBuffer) into a GLB Blob. + * GLB files are returned as-is from their original blob. + */ +export async function convertToGLB( + buffer: ArrayBuffer, + ext: 'obj' | 'stl', + fileName: string +): Promise<{ blob: Blob; fileName: string; fileSize: number }> { + const scene = new THREE.Scene(); + + const material = new THREE.MeshStandardMaterial({ + color: 0x8899aa, + metalness: 0.85, + roughness: 0.35, + }); + + if (ext === 'stl') { + const loader = new STLLoader(); + const geometry = loader.parse(buffer); + geometry.computeVertexNormals(); + const mesh = new THREE.Mesh(geometry, material); + mesh.name = fileName.replace(/\.stl$/i, ''); + scene.add(mesh); + } else { + const loader = new OBJLoader(); + const text = new TextDecoder().decode(buffer); + const obj = loader.parse(text); + obj.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.material = material; + } + }); + obj.name = fileName.replace(/\.obj$/i, ''); + scene.add(obj); + } + + const exporter = new GLTFExporter(); + const glb = await new Promise((resolve, reject) => { + exporter.parse( + scene, + (result) => resolve(result as ArrayBuffer), + (error) => reject(error), + { binary: true } + ); + }); + + const glbFileName = fileName.replace(/\.(obj|stl)$/i, '.glb'); + const blob = new Blob([glb], { type: 'model/gltf-binary' }); + + return { blob, fileName: glbFileName, fileSize: blob.size }; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})