🚀 Auto-deploy: melhoria no snap e medição AR em 01/06/2026 00:41:59

This commit is contained in:
2026-06-01 00:41:59 +00:00
parent ba33180a00
commit b47fb872b5
4 changed files with 271 additions and 7 deletions
+73 -1
View File
@@ -9,6 +9,19 @@ import { registerModelLocalGroup, unregisterModelLocalGroup, getModelWorldScaleF
import { parseIFCtoThree } from '@/lib/convertIFC'; import { parseIFCtoThree } from '@/lib/convertIFC';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { mainCameraRef, mainControlsRef, viewAnim, calibration, pushModelFaceNormal, computeCalibrationQuaternion, subscribeCalibration } from './viewCubeBus'; import { mainCameraRef, mainControlsRef, viewAnim, calibration, pushModelFaceNormal, computeCalibrationQuaternion, subscribeCalibration } from './viewCubeBus';
import { toast } from 'sonner';
function getColorForMaterialName(name: string): string {
if (!name) return '#a1a1aa';
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
const h = Math.abs(hash) % 360;
const s = 65 + (Math.abs(hash >> 8) % 15);
const l = 45 + (Math.abs(hash >> 16) % 10);
return `hsl(${h}, ${s}%, ${l}%)`;
}
interface ModelViewerProps { interface ModelViewerProps {
url?: string; // legacy, ignored — uses store.models url?: string; // legacy, ignored — uses store.models
@@ -195,6 +208,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
const setActive = useModelStore((s) => s.setActiveModel); const setActive = useModelStore((s) => s.setActiveModel);
const selectionMode = useModelStore((s) => s.selectionMode); const selectionMode = useModelStore((s) => s.selectionMode);
const measureMode = useModelStore((s) => s.measureMode); const measureMode = useModelStore((s) => s.measureMode);
const ifcMaterialColors = useModelStore((s) => s.ifcMaterialColors);
const fineTuning = sceneModel.fineTuning; const fineTuning = sceneModel.fineTuning;
// Re-render when calibration state changes (so calQuat resets to identity during the flow). // Re-render when calibration state changes (so calQuat resets to identity during the flow).
@@ -260,11 +274,17 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
mat.color.setHSL(0, 0.7, 0.5); mat.color.setHSL(0, 0.7, 0.5);
} else if (allApproved) { } else if (allApproved) {
mat.color.setHSL(0.38, 0.7, 0.45); mat.color.setHSL(0.38, 0.7, 0.45);
} else {
const root = findElementRoot(child);
const matName = root?.userData?.materialName;
if (ifcMaterialColors && matName) {
mat.color.set(getColorForMaterialName(matName));
} else { } else {
// Tint with the per-model color (subtle) // Tint with the per-model color (subtle)
mat.color.set(sceneModel.color); mat.color.set(sceneModel.color);
} }
} }
}
mat.needsUpdate = true; mat.needsUpdate = true;
} }
}); });
@@ -284,7 +304,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
} }
} }
}); });
}, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle, sceneModel.color, measureMode]); }, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle, sceneModel.color, measureMode, ifcMaterialColors]);
const rotXRad = (fineTuning.rotX * Math.PI) / 180; const rotXRad = (fineTuning.rotX * Math.PI) / 180;
const rotYRad = (fineTuning.rotY * Math.PI) / 180; const rotYRad = (fineTuning.rotY * Math.PI) / 180;
@@ -716,6 +736,7 @@ function HoverDetector() {
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const lastHitKey = useRef(''); const lastHitKey = useRef('');
const lastHitExpressId = useRef<number>(0);
useEffect(() => { useEffect(() => {
const clearTimers = () => { const clearTimers = () => {
@@ -741,11 +762,59 @@ function HoverDetector() {
if (!hit || !(hit.object instanceof THREE.Mesh)) { if (!hit || !(hit.object instanceof THREE.Mesh)) {
lastHitKey.current = ''; lastHitKey.current = '';
lastHitExpressId.current = 0;
setHoverInfo(null); setHoverInfo(null);
clearTimers(); clearTimers();
toast.dismiss('ifc-hover-info');
return; return;
} }
// Detecção de propriedades do IFC de forma ágil baseada em mudança de elemento
const elementRoot = findElementRoot(hit.object);
const expressID = elementRoot?.userData?.ifcId ?? 0;
if (expressID !== lastHitExpressId.current) {
lastHitExpressId.current = expressID;
const props = elementRoot?.userData?.properties;
if (props && (props.name || props.material || props.tag)) {
const infoLines: string[] = [];
if (props.tag || props.name) {
infoLines.push(`Peça: ${props.tag || props.name}`);
}
if (props.material) {
infoLines.push(`Material: ${props.material}`);
}
const lengthKey = Object.keys(props).find(k => {
const kl = k.toLowerCase();
return kl.includes('length') || kl.includes('comprimento') || kl.includes('lengthvalue');
});
if (lengthKey && props[lengthKey]) {
infoLines.push(`Comprimento: ${props[lengthKey]}`);
} else {
const commonLengthKeys = ['Length', 'Comprimento', 'LengthValue', 'height', 'comprimento'];
for (const key of commonLengthKeys) {
if (props[key]) {
infoLines.push(`Comprimento: ${props[key]}`);
break;
}
}
}
if (infoLines.length > 0) {
toast(infoLines.join(' | '), {
id: 'ifc-hover-info',
duration: 8000,
position: 'bottom-center',
});
}
} else {
toast.dismiss('ifc-hover-info');
}
}
// Stability check same approximate position for debounce (~3 mm) // Stability check same approximate position for debounce (~3 mm)
const key = `${hit.point.x.toFixed(3)},${hit.point.y.toFixed(3)},${hit.point.z.toFixed(3)}`; const key = `${hit.point.x.toFixed(3)},${hit.point.y.toFixed(3)},${hit.point.z.toFixed(3)}`;
if (key === lastHitKey.current) return; // timer already running if (key === lastHitKey.current) return; // timer already running
@@ -808,8 +877,10 @@ function HoverDetector() {
const onLeave = () => { const onLeave = () => {
lastHitKey.current = ''; lastHitKey.current = '';
lastHitExpressId.current = 0;
setHoverInfo(null); setHoverInfo(null);
clearTimers(); clearTimers();
toast.dismiss('ifc-hover-info');
}; };
gl.domElement.addEventListener('pointermove', onMove); gl.domElement.addEventListener('pointermove', onMove);
@@ -818,6 +889,7 @@ function HoverDetector() {
gl.domElement.removeEventListener('pointermove', onMove); gl.domElement.removeEventListener('pointermove', onMove);
gl.domElement.removeEventListener('pointerleave', onLeave); gl.domElement.removeEventListener('pointerleave', onLeave);
clearTimers(); clearTimers();
toast.dismiss('ifc-hover-info');
}; };
}, [camera, scene, gl, raycaster, mouse, setHoverInfo]); }, [camera, scene, gl, raycaster, mouse, setHoverInfo]);
+158 -3
View File
@@ -7,6 +7,53 @@ const WASM_PATH = 'https://unpkg.com/web-ifc@0.0.57/';
/** /**
* Convert an IFC file (ArrayBuffer) into a GLB Blob using web-ifc + GLTFExporter. * 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<THREE.Scene> { export async function parseIFCtoThree(buffer: ArrayBuffer): Promise<THREE.Scene> {
const ifcApi = new WebIFC.IfcAPI(); const ifcApi = new WebIFC.IfcAPI();
ifcApi.SetWasmPath(WASM_PATH, true); ifcApi.SetWasmPath(WASM_PATH, true);
@@ -15,6 +62,87 @@ export async function parseIFCtoThree(buffer: ArrayBuffer): Promise<THREE.Scene>
const data = new Uint8Array(buffer); const data = new Uint8Array(buffer);
const modelID = ifcApi.OpenModel(data); const modelID = ifcApi.OpenModel(data);
// Mapeamento de materiais
const elementMaterialMap = new Map<number, string>();
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<number, Record<string, string>>();
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<string, string> = {};
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)}`;
else if (qLine.VolumeValue !== undefined) value = `${qLine.VolumeValue.toFixed(2)}`;
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 scene = new THREE.Scene();
const materials: Map<number, THREE.MeshStandardMaterial> = new Map(); const materials: Map<number, THREE.MeshStandardMaterial> = new Map();
@@ -22,11 +150,38 @@ export async function parseIFCtoThree(buffer: ArrayBuffer): Promise<THREE.Scene>
const placedGeometries = mesh.geometries; const placedGeometries = mesh.geometries;
const expressID = (mesh as unknown as { expressID?: number }).expressID ?? 0; const expressID = (mesh as unknown as { expressID?: number }).expressID ?? 0;
// One Group per IfcProduct → represents a single "element" (beam, plate…). let name = '';
// userData is preserved by GLTFExporter as `extras` and survives reload. 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(); const elementGroup = new THREE.Group();
elementGroup.name = `ifc_${expressID}`; elementGroup.name = `ifc_${expressID}`;
elementGroup.userData = { ifcElement: true, ifcId: expressID }; elementGroup.userData = {
ifcElement: true,
ifcId: expressID,
materialName,
properties: {
name,
tag,
objectType,
description,
material: materialName,
...extraProps
}
};
for (let i = 0; i < placedGeometries.size(); i++) { for (let i = 0; i < placedGeometries.size(); i++) {
const placedGeometry = placedGeometries.get(i); const placedGeometry = placedGeometries.get(i);
+18 -1
View File
@@ -24,7 +24,7 @@ const Index = () => {
const [showLanding, setShowLanding] = useState(false); const [showLanding, setShowLanding] = useState(false);
const [converting, setConverting] = useState(false); const [converting, setConverting] = useState(false);
const { model, addModel, models, maxModels, xrSupported, setXrSupported } = useModelStore(); const { model, addModel, models, maxModels, xrSupported, setXrSupported, ifcMaterialColors, setIfcMaterialColors } = useModelStore();
// --- Painel de Logs Remotos em Tempo Real --- // --- Painel de Logs Remotos em Tempo Real ---
const [debugLogs, setDebugLogs] = useState<{ level: 'info' | 'warn' | 'error'; message: string; timestamp: string; data?: unknown }[]>([]); const [debugLogs, setDebugLogs] = useState<{ level: 'info' | 'warn' | 'error'; message: string; timestamp: string; data?: unknown }[]>([]);
@@ -265,6 +265,23 @@ const Index = () => {
/> />
</div> </div>
{/* Toggle IFC Material Colors */}
<div className="flex items-center justify-between rounded-lg border border-border bg-card/50 p-3.5">
<div className="flex flex-col gap-0.5">
<Label htmlFor="ifc-mat-colors-toggle" className="text-xs font-semibold cursor-pointer text-foreground">
Cores por Material (IFC)
</Label>
<p className="text-[10px] text-muted-foreground">
{ifcMaterialColors ? 'Cores baseadas no material do IFC' : 'Cores padrão da cena'}
</p>
</div>
<Switch
id="ifc-mat-colors-toggle"
checked={ifcMaterialColors}
onCheckedChange={setIfcMaterialColors}
/>
</div>
{/* Demo buttons */} {/* Demo buttons */}
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
<CloudLoader /> <CloudLoader />
+20
View File
@@ -94,6 +94,18 @@ function loadVisibility(fileName: string): { hidden: Set<string>; isolated: Set<
return { hidden: new Set(v.hidden ?? []), isolated: v.isolated ? new Set(v.isolated) : null }; return { hidden: new Set(v.hidden ?? []), isolated: v.isolated ? new Set(v.isolated) : null };
} }
const IFC_MAT_COLORS_KEY = 'tsxr_ifc_mat_colors_v1';
function loadIfcMaterialColors(): boolean {
try {
const raw = localStorage.getItem(IFC_MAT_COLORS_KEY);
return raw !== 'false';
} catch (e) { /* ignore */ }
return true;
}
function saveIfcMaterialColors(enabled: boolean) {
try { localStorage.setItem(IFC_MAT_COLORS_KEY, String(enabled)); } catch (e) { /* ignore */ }
}
export interface ScaleRatio { export interface ScaleRatio {
@@ -354,6 +366,9 @@ interface ModelStore {
xrFeatures: XRFeatureFlags; xrFeatures: XRFeatureFlags;
setXRFeature: (key: keyof XRFeatureFlags, value: boolean) => void; setXRFeature: (key: keyof XRFeatureFlags, value: boolean) => void;
resetXRFeatures: () => void; resetXRFeatures: () => void;
ifcMaterialColors: boolean;
setIfcMaterialColors: (enabled: boolean) => void;
} }
/** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */ /** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */
@@ -785,4 +800,9 @@ export const useModelStore = create<ModelStore>((set, get) => ({
saveXRFeatures(next); saveXRFeatures(next);
set({ xrFeatures: next }); set({ xrFeatures: next });
}, },
ifcMaterialColors: loadIfcMaterialColors(),
setIfcMaterialColors: (enabled: boolean) => {
saveIfcMaterialColors(enabled);
set({ ifcMaterialColors: enabled });
},
})); }));