🚀 Auto-deploy: melhoria no snap e medição AR em 01/06/2026 00:41:59
This commit is contained in:
@@ -9,6 +9,19 @@ import { registerModelLocalGroup, unregisterModelLocalGroup, getModelWorldScaleF
|
||||
import { parseIFCtoThree } from '@/lib/convertIFC';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
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 {
|
||||
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 selectionMode = useModelStore((s) => s.selectionMode);
|
||||
const measureMode = useModelStore((s) => s.measureMode);
|
||||
const ifcMaterialColors = useModelStore((s) => s.ifcMaterialColors);
|
||||
const fineTuning = sceneModel.fineTuning;
|
||||
|
||||
// Re-render when calibration state changes (so calQuat resets to identity during the flow).
|
||||
@@ -261,8 +275,14 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
|
||||
} else if (allApproved) {
|
||||
mat.color.setHSL(0.38, 0.7, 0.45);
|
||||
} else {
|
||||
// Tint with the per-model color (subtle)
|
||||
mat.color.set(sceneModel.color);
|
||||
const root = findElementRoot(child);
|
||||
const matName = root?.userData?.materialName;
|
||||
if (ifcMaterialColors && matName) {
|
||||
mat.color.set(getColorForMaterialName(matName));
|
||||
} else {
|
||||
// Tint with the per-model color (subtle)
|
||||
mat.color.set(sceneModel.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 rotYRad = (fineTuning.rotY * Math.PI) / 180;
|
||||
@@ -716,6 +736,7 @@ function HoverDetector() {
|
||||
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const lastHitKey = useRef('');
|
||||
const lastHitExpressId = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const clearTimers = () => {
|
||||
@@ -741,11 +762,59 @@ function HoverDetector() {
|
||||
|
||||
if (!hit || !(hit.object instanceof THREE.Mesh)) {
|
||||
lastHitKey.current = '';
|
||||
lastHitExpressId.current = 0;
|
||||
setHoverInfo(null);
|
||||
clearTimers();
|
||||
toast.dismiss('ifc-hover-info');
|
||||
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)
|
||||
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
|
||||
@@ -808,8 +877,10 @@ function HoverDetector() {
|
||||
|
||||
const onLeave = () => {
|
||||
lastHitKey.current = '';
|
||||
lastHitExpressId.current = 0;
|
||||
setHoverInfo(null);
|
||||
clearTimers();
|
||||
toast.dismiss('ifc-hover-info');
|
||||
};
|
||||
|
||||
gl.domElement.addEventListener('pointermove', onMove);
|
||||
@@ -818,6 +889,7 @@ function HoverDetector() {
|
||||
gl.domElement.removeEventListener('pointermove', onMove);
|
||||
gl.domElement.removeEventListener('pointerleave', onLeave);
|
||||
clearTimers();
|
||||
toast.dismiss('ifc-hover-info');
|
||||
};
|
||||
}, [camera, scene, gl, raycaster, mouse, setHoverInfo]);
|
||||
|
||||
|
||||
+158
-3
@@ -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.
|
||||
*/
|
||||
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> {
|
||||
const ifcApi = new WebIFC.IfcAPI();
|
||||
ifcApi.SetWasmPath(WASM_PATH, true);
|
||||
@@ -15,6 +62,87 @@ export async function parseIFCtoThree(buffer: ArrayBuffer): Promise<THREE.Scene>
|
||||
const data = new Uint8Array(buffer);
|
||||
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)} 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<number, THREE.MeshStandardMaterial> = new Map();
|
||||
|
||||
@@ -22,11 +150,38 @@ export async function parseIFCtoThree(buffer: ArrayBuffer): Promise<THREE.Scene>
|
||||
const placedGeometries = mesh.geometries;
|
||||
const expressID = (mesh as unknown as { expressID?: number }).expressID ?? 0;
|
||||
|
||||
// One Group per IfcProduct → represents a single "element" (beam, plate…).
|
||||
// userData is preserved by GLTFExporter as `extras` and survives reload.
|
||||
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 };
|
||||
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);
|
||||
|
||||
+18
-1
@@ -24,7 +24,7 @@ const Index = () => {
|
||||
const [showLanding, setShowLanding] = 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 ---
|
||||
const [debugLogs, setDebugLogs] = useState<{ level: 'info' | 'warn' | 'error'; message: string; timestamp: string; data?: unknown }[]>([]);
|
||||
@@ -265,6 +265,23 @@ const Index = () => {
|
||||
/>
|
||||
</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 */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<CloudLoader />
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -354,6 +366,9 @@ interface ModelStore {
|
||||
xrFeatures: XRFeatureFlags;
|
||||
setXRFeature: (key: keyof XRFeatureFlags, value: boolean) => void;
|
||||
resetXRFeatures: () => void;
|
||||
|
||||
ifcMaterialColors: boolean;
|
||||
setIfcMaterialColors: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
/** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */
|
||||
@@ -785,4 +800,9 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
||||
saveXRFeatures(next);
|
||||
set({ xrFeatures: next });
|
||||
},
|
||||
ifcMaterialColors: loadIfcMaterialColors(),
|
||||
setIfcMaterialColors: (enabled: boolean) => {
|
||||
saveIfcMaterialColors(enabled);
|
||||
set({ ifcMaterialColors: enabled });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user