hit test edgeId detection

This commit is contained in:
azykov@mail.ru 2026-05-24 15:18:46 +03:00
parent e8c635c139
commit 0354391f96
No known key found for this signature in database
6 changed files with 69 additions and 29 deletions

View File

@ -1,14 +1,15 @@
import type { Face, Mesh, Solid, Surface, Vertex } from "../../types"; import type { Edge, Face, HalfEdge, Mesh, Solid, Surface, Vertex } from "../../types";
export type MeshDto = { export type MeshDto = {
vertices: Float32Array; vertices: Float32Array;
normals: Float32Array normals: Float32Array
indices: Uint16Array; indices: Uint16Array;
vertexIds: Vertex['id'][]; // vertexIds: Vertex['id'][];
faceId: Face['id']; faceId: Face['id'];
surfaceId: Surface['id']; surfaceId: Surface['id'];
solidId: Solid['id']; solidId: Solid['id'];
loop: { edge: Edge['id'], halfEdge: HalfEdge['id'], vertex: Vertex['id'], vertex2: Vertex['id'] }[];
}; };
export function meshToDto(mesh: Mesh): MeshDto { export function meshToDto(mesh: Mesh): MeshDto {
@ -16,9 +17,10 @@ export function meshToDto(mesh: Mesh): MeshDto {
vertices: new Float32Array(mesh.vertices.flat()), vertices: new Float32Array(mesh.vertices.flat()),
normals: new Float32Array(mesh.normals.flat()), normals: new Float32Array(mesh.normals.flat()),
indices: new Uint16Array(mesh.indices.flat()), indices: new Uint16Array(mesh.indices.flat()),
vertexIds: mesh.vertexIds, // vertexIds: mesh.vertexIds,
faceId: mesh.faceId, faceId: mesh.faceId,
surfaceId: mesh.surfaceId, surfaceId: mesh.surfaceId,
solidId: mesh.solidId, solidId: mesh.solidId,
loop: mesh.loop,
}; };
} }

View File

@ -1,6 +1,6 @@
import earcut from 'earcut'; import earcut from 'earcut';
import type { Face, Mesh, V2, V3, Vertex } from "../../types"; import type { Edge, Face, HalfEdge, Mesh, V2, V3, Vertex } from "../../types";
import { db } from "../db"; import { db } from "../db";
export type Basis3D = { origin: V3; u: V3; v: V3; normal: V3; }; export type Basis3D = { origin: V3; u: V3; v: V3; normal: V3; };
@ -48,7 +48,10 @@ function getNormal(vertices: V3[]): V3 {
export class PlaneTessellator { export class PlaneTessellator {
public static tessellate(face: Face): Omit<Mesh, 'faceId' | 'surfaceId' | 'solidId'>[] { public static tessellate(face: Face): Omit<Mesh, 'faceId' | 'surfaceId' | 'solidId'>[] {
const vertices3d = PlaneTessellator.getVerticesByLoop(face.outerLoop); const loop = PlaneTessellator.getLoop(face.outerLoop);
const loopWithV2s = loop.map((v, idx) => ({ ...v, vertex2: loop[(idx + 1) % loop.length].vertex }));
const vertices3d = loop.map((v) => v.vertex);
const basis = PlaneTessellator.getBasis(vertices3d); const basis = PlaneTessellator.getBasis(vertices3d);
const vertices2d = PlaneTessellator.projectVertices(vertices3d, basis); const vertices2d = PlaneTessellator.projectVertices(vertices3d, basis);
@ -60,11 +63,12 @@ export class PlaneTessellator {
vertices: vertices3d.map((v) => [v.x, v.y, v.z]), vertices: vertices3d.map((v) => [v.x, v.y, v.z]),
normals: vertices3d.map(() => basis.normal), normals: vertices3d.map(() => basis.normal),
indices, indices,
vertexIds: vertices3d.map((v) => v.id), // vertexIds: vertices3d.map((v) => v.id),
loop: loopWithV2s.map((v) => ({ halfEdge: v.halfEdge.id, edge: v.edge.id, vertex: v.vertex.id, vertex2: v.vertex2.id })),
}]; }];
} }
private static getVerticesByLoop(loopId: string): Vertex[] { private static getLoop(loopId: string): { halfEdge: HalfEdge, edge: Edge, vertex: Vertex }[] {
const halfEdges = db.halfEdgesByLoop(loopId) const halfEdges = db.halfEdgesByLoop(loopId)
.map((he) => ({ .map((he) => ({
halfedge: he, halfedge: he,
@ -74,7 +78,11 @@ export class PlaneTessellator {
throw new Error(`Loop ${loopId}: only linear half-edges are supported for plane tesselation`); throw new Error(`Loop ${loopId}: only linear half-edges are supported for plane tesselation`);
return halfEdges return halfEdges
.map((he) => db.vertexById(he.halfedge.origin)!); .map((halfEdge, idx) => ({
halfEdge: halfEdge.halfedge,
edge: halfEdge.edge,
vertex: db.vertexById(halfEdge.halfedge.origin)!,
}));
} }
public static projectVertices(vertices: Vertex[], basis: Basis3D): V2[] { public static projectVertices(vertices: Vertex[], basis: Basis3D): V2[] {

View File

@ -2,6 +2,7 @@ import * as THREE from 'three';
import { CONTAINED, ExtendedTriangle, INTERSECTED, NOT_INTERSECTED } from 'three-mesh-bvh'; import { CONTAINED, ExtendedTriangle, INTERSECTED, NOT_INTERSECTED } from 'three-mesh-bvh';
import { CircularFrustum } from './circularFrustum'; import { CircularFrustum } from './circularFrustum';
import type { Id } from '../types'; import type { Id } from '../types';
import type { MeshDto } from '../backend/dto';
export type TriangleVertexHitDetail = { export type TriangleVertexHitDetail = {
kind: 'vertex', kind: 'vertex',
@ -11,7 +12,8 @@ export type TriangleVertexHitDetail = {
export type TriangleEdgeHitDetail = { export type TriangleEdgeHitDetail = {
kind: 'edge', kind: 'edge',
index: 0 | 1 | 2, // edge 0=AB, 1=BC, 2=CA index: 0 | 1 | 2, // edge 0=AB, 1=BC, 2=CA
id?: Id, aId?: Id,
bId?: Id,
} }
export type TriangleFaceHitDetail = { export type TriangleFaceHitDetail = {
kind: 'face', kind: 'face',
@ -113,9 +115,9 @@ function classifyTriangleHit(
const onBC = u < BARYCENTRIC_EPSILON; const onBC = u < BARYCENTRIC_EPSILON;
const onCA = v < BARYCENTRIC_EPSILON; const onCA = v < BARYCENTRIC_EPSILON;
if (onAB) return { kind: 'edge', index: 0, id: `${vertexIds?.[0]}-${vertexIds?.[1]}` }; if (onAB) return { kind: 'edge', index: 0, aId: vertexIds?.[0], bId: vertexIds?.[1] };
if (onBC) return { kind: 'edge', index: 1, id: `${vertexIds?.[1]}-${vertexIds?.[2]}` }; if (onBC) return { kind: 'edge', index: 1, aId: vertexIds?.[1], bId: vertexIds?.[2] };
if (onCA) return { kind: 'edge', index: 2, id: `${vertexIds?.[2]}-${vertexIds?.[0]}` }; if (onCA) return { kind: 'edge', index: 2, aId: vertexIds?.[2], bId: vertexIds?.[0] };
return { kind: 'face' }; return { kind: 'face' };
} }
@ -380,7 +382,9 @@ export class CircularFrustumIntersection {
} }
private intersectionToHitResult(intersection: Intersection): HitResult[] { private intersectionToHitResult(intersection: Intersection): HitResult[] {
const faceId = intersection.object.userData.faceId; const userData = intersection.object.userData as MeshDto;
const faceId = userData.faceId;
const loop = userData.loop;
const results: HitResult[] = [{ const results: HitResult[] = [{
kind: 'face', kind: 'face',
@ -392,15 +396,18 @@ export class CircularFrustumIntersection {
}, },
}]; }];
if (intersection.triHit?.kind === 'edge') { if (intersection.triHit?.kind === 'edge') {
results.unshift({ const triHit = intersection.triHit;
kind: 'edge', const edge = loop.find((v) => (v.vertex === triHit.aId) && (v.vertex2 === triHit.bId))?.edge;
id: intersection.triHit?.id, if (edge !== undefined)
faceId, results.unshift({
intersection: { kind: 'edge',
...intersection, id: edge,
triHit: undefined, faceId,
}, intersection: {
}); ...intersection,
triHit: undefined,
},
});
} }
if (intersection.triHit?.kind === 'vertex') { if (intersection.triHit?.kind === 'vertex') {
results.unshift({ results.unshift({

View File

@ -16,6 +16,7 @@ export class SceneSync {
side: THREE.DoubleSide, side: THREE.DoubleSide,
transparent: true, transparent: true,
depthWrite: false, depthWrite: false,
wireframe: true,
uniforms: { uniforms: {
frontColor: { value: new THREE.Color(0x88ccff) }, frontColor: { value: new THREE.Color(0x88ccff) },
frontOpacity: { value: 0.6 }, frontOpacity: { value: 0.6 },
@ -90,10 +91,8 @@ export class SceneSync {
const geoVCount = geo.attributes.position.count; const geoVCount = geo.attributes.position.count;
geo.setAttribute('selected', new THREE.BufferAttribute(new Float32Array(geoVCount), 1)); geo.setAttribute('selected', new THREE.BufferAttribute(new Float32Array(geoVCount), 1));
const mesh = new THREE.Mesh(geo, this.baseMaterial); const mesh = new THREE.Mesh(geo, this.baseMaterial);
mesh.userData.faceId = faceId; mesh.userData = dto
mesh.userData.vertexIds = dto.vertexIds; mesh.userData.vertexIds = dto.loop.map((v) => v.vertex);
mesh.userData.surfaceId = dto.surfaceId;
mesh.userData.solidId = dto.solidId;
this.scene.add(mesh); this.scene.add(mesh);
this.meshByFace[faceId] = mesh; this.meshByFace[faceId] = mesh;

View File

@ -54,6 +54,14 @@ export class Model {
return this.halfEdges[id]; return this.halfEdges[id];
} }
public halfEdgesByFilter(filter: (halfEdge: HalfEdge) => boolean): HalfEdge[] {
return Object.values(this.halfEdges).filter(filter);
}
public halfEdgesByVertexId(id: Vertex['id']): HalfEdge[] {
return this.halfEdgesByFilter((he) => he.origin === id);
}
public halfEdgesByLoop(loopId: string): HalfEdge[] { public halfEdgesByLoop(loopId: string): HalfEdge[] {
const loop = this.loopById(loopId)!; const loop = this.loopById(loopId)!;
const startHalfEdgeId = loop.start; const startHalfEdgeId = loop.start;
@ -76,6 +84,10 @@ export class Model {
return this.edges[id]; return this.edges[id];
} }
public edgesByFilter(filter: (edge: Edge) => boolean): Edge[] {
return Object.values(this.edges).filter(filter);
}
public loopById(id: Loop['id']): Loop | undefined { public loopById(id: Loop['id']): Loop | undefined {
return this.loops[id]; return this.loops[id];
} }
@ -91,6 +103,19 @@ export class Model {
public solidById(id: Solid['id']): Solid | undefined { public solidById(id: Solid['id']): Solid | undefined {
return this.solids[id]; return this.solids[id];
} }
public edgesByVertexIds(a: Vertex['id'], b: Vertex['id'], belongsToFaceId?: Face['id']): Edge[] {
let hesA = this.halfEdgesByVertexId(a);
let hesB = this.halfEdgesByVertexId(b);
const edges = this.edgesByFilter((e) => (
(e.a ? hesA.some((he) => he.id === a) : true) && (e.b ? hesB.some((he) => he.id === b) : true)
||
(e.a ? hesB.some((he) => he.id === a) : true) && (e.b ? hesA.some((he) => he.id === b) : true)
));
return edges;
}
} }
export const model = new Model(); export const model = new Model();

View File

@ -1,16 +1,15 @@
import type { Face, Solid, Surface, Vertex } from "./brep"; import type { Edge, Face, HalfEdge, Solid, Surface, Vertex } from "./brep";
export type V2 = [x: number, y: number]; export type V2 = [x: number, y: number];
export type V3 = [x: number, y: number, z: number]; export type V3 = [x: number, y: number, z: number];
// export type V3 = [number, number, number];
export type Mesh = { export type Mesh = {
vertices: V3[]; vertices: V3[];
normals: V3[]; normals: V3[];
indices: number[]; indices: number[];
vertexIds: Vertex['id'][];
faceId: Face['id']; faceId: Face['id'];
surfaceId: Surface['id']; surfaceId: Surface['id'];
solidId: Solid['id']; solidId: Solid['id'];
loop: { edge: Edge['id'], halfEdge: HalfEdge['id'], vertex: Vertex['id'] }[];
}; };