UNPKG

@pmndrs/uikit

Version:

Build performant 3D user interfaces with Three.js and yoga.

238 lines (237 loc) 8.96 kB
import { computed } from '@preact/signals-core'; import { Plane, Vector3 } from 'three'; import { Overflow } from 'yoga-layout/load'; const dotLt45deg = Math.cos((45 / 180) * Math.PI); const helperPlanes = [new Plane(), new Plane(), new Plane(), new Plane()]; const positionHelper = new Vector3(); export class ClippingRect { planes; facePlane; originalCenter; constructor(globalMatrix, centerX, centerY, width, height) { this.originalCenter = new Vector3(centerX, centerY, 0).applyMatrix4(globalMatrix); this.facePlane = new Plane(new Vector3(0, 0, 1), 0).applyMatrix4(globalMatrix); const halfWidth = width / 2; const halfHeight = height / 2; const top = centerY + halfHeight; const right = centerX + halfWidth; const bottom = -centerY + halfHeight; const left = -centerX + halfWidth; this.planes = [ new Plane(new Vector3(0, -1, 0), bottom).applyMatrix4(globalMatrix), new Plane(new Vector3(-1, 0, 0), left).applyMatrix4(globalMatrix), new Plane(new Vector3(0, 1, 0), top).applyMatrix4(globalMatrix), new Plane(new Vector3(1, 0, 0), right).applyMatrix4(globalMatrix), ]; } min({ planes }) { for (let i = 0; i < 4; i++) { const p1 = this.facePlane; const p2 = planes[i]; const n1n2DotProduct = p1.normal.dot(p2.normal); if (Math.abs(n1n2DotProduct) > 0.99) { return this; //projection unsuccessfull => clipping rect is 90 deg rotated } const helperPlane = helperPlanes[i]; if (Math.abs(n1n2DotProduct) < 0.01) { //projection unnecassary => already correctly projected helperPlane.copy(p2); continue; } helperPlane.normal.crossVectors(p1.normal, p2.normal).normalize().cross(p1.normal).negate(); //from: https://en.wikipedia.org/wiki/Plane%E2%80%93plane_intersection const divisor = 1 - n1n2DotProduct * n1n2DotProduct; const c1 = (p1.constant - p2.constant * n1n2DotProduct) / divisor; const c2 = (p2.constant - p1.constant * n1n2DotProduct) / divisor; positionHelper.copy(p1.normal).multiplyScalar(c1).addScaledVector(p2.normal, c2); helperPlane.constant = -positionHelper.dot(helperPlane.normal); } //2. step: find index offset (e.g. if the child was rotate by 90deg in z-axis) let indexOffset = 0; const firstPlaneNormal = this.planes[0].normal; while (helperPlanes[indexOffset].normal.dot(firstPlaneNormal) > dotLt45deg) { break; } //3. step: minimize (if the helper plane is smaller => copy from the planes because they have the original orientation) for (let i = 0; i < 4; i++) { const plane = this.planes[i]; const otherPlaneIndex = (i + indexOffset) % 4; if (helperPlanes[otherPlaneIndex].distanceToPoint(this.originalCenter) < plane.distanceToPoint(this.originalCenter)) { plane.copy(planes[otherPlaneIndex]); } } return this; } toArray(array, offset) { for (let i = 0; i < 4; i++) { const { normal, constant } = this.planes[i]; normal.toArray(array, offset); array[offset + 3] = constant; offset += 4; } } } const helperPoints = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; const multiplier = [ [-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5], ]; export function computedIsClipped(parent, globalMatrix, size, pixelSizeSignal) { return computed(() => { if (parent.value == null) { return false; } if (size.value == null) { return true; } const global = globalMatrix.value; const rect = parent.value.clippingRect.value; if (rect == null || global == null) { return false; } const [width, height] = size.value; const pixelSize = pixelSizeSignal.value; for (let i = 0; i < 4; i++) { const [mx, my] = multiplier[i]; helperPoints[i].set(mx * pixelSize * width, my * pixelSize * height, 0).applyMatrix4(global); } const { planes } = rect; let allOutside; for (let planeIndex = 0; planeIndex < 4; planeIndex++) { const clippingPlane = planes[planeIndex]; allOutside = true; for (let pointIndex = 0; pointIndex < 4; pointIndex++) { const point = helperPoints[pointIndex]; if (clippingPlane.distanceToPoint(point) >= 0) { //inside allOutside = false; } } if (allOutside) { return true; } } return false; }); } export function computedClippingRect(globalMatrix, { overflow, borderInset, size }, pixelSizeSignal, parentClippingRect) { return computed(() => { const global = globalMatrix.value; if (global == null || overflow.value === Overflow.Visible) { return parentClippingRect?.value; } if (size.value == null || borderInset.value == null) { return undefined; } const [width, height] = size.value; const [top, right, bottom, left] = borderInset.value; const pixelSize = pixelSizeSignal.value; const rect = new ClippingRect(global, ((right - left) * pixelSize) / 2, ((top - bottom) * pixelSize) / 2, (width - left - right) * pixelSize, (height - top - bottom) * pixelSize); if (parentClippingRect?.value != null) { rect.min(parentClippingRect.value); } return rect; }); } export const NoClippingPlane = new Plane(new Vector3(-1, 0, 0), Number.MAX_SAFE_INTEGER); export const defaultClippingData = new Float32Array(16); for (let i = 0; i < 4; i++) { NoClippingPlane.normal.toArray(defaultClippingData, i * 4); defaultClippingData[i * 4 + 3] = NoClippingPlane.constant; } export function createGlobalClippingPlanes(component) { const getGlobalMatrix = () => component.root.peek().component.parent?.matrixWorld; const planes = new Array(4) .fill(undefined) .map((_, i) => new RelativePlane(() => component.parentContainer.peek()?.clippingRect.value?.planes[i], getGlobalMatrix)); return planes; } const helperPlane = new Plane(); class RelativePlane { getLocalPlane; getGlobalMatrix; get normal() { this.computeInto(helperPlane); return helperPlane.normal; } get constant() { this.computeInto(helperPlane); return helperPlane.constant; } isPlane = true; constructor(getLocalPlane, getGlobalMatrix) { this.getLocalPlane = getLocalPlane; this.getGlobalMatrix = getGlobalMatrix; } computeInto(target) { const localPlane = this.getLocalPlane(); const globalMatrix = this.getGlobalMatrix(); if (localPlane == null || globalMatrix == null) { return target.copy(NoClippingPlane); } return target.copy(localPlane).applyMatrix4(globalMatrix); } set(normal, constant) { return this; } setComponents(x, y, z, w) { return this; } setFromNormalAndCoplanarPoint(normal, point) { return this; } setFromCoplanarPoints(a, b, c) { return this; } clone() { return this.computeInto(new Plane()); } copy(plane) { this.computeInto(plane); return this; } normalize() { return this; } negate() { return this; } distanceToPoint(point) { return this.computeInto(helperPlane).distanceToPoint(point); } distanceToSphere(sphere) { return this.computeInto(helperPlane).distanceToSphere(sphere); } projectPoint(point, target) { return this.computeInto(helperPlane).projectPoint(point, target); } intersectLine(line, target) { return this.computeInto(helperPlane).intersectLine(line, target); } intersectsLine(line) { return this.computeInto(helperPlane).intersectsLine(line); } intersectsBox(box) { return this.computeInto(helperPlane).intersectsBox(box); } intersectsSphere(sphere) { return this.computeInto(helperPlane).intersectsSphere(sphere); } coplanarPoint(target) { return this.computeInto(helperPlane).coplanarPoint(target); } applyMatrix4(matrix, optionalNormalMatrix) { return this; } translate(offset) { return this; } equals(plane) { return this.computeInto(helperPlane).equals(plane); } isIntersectionLine(l) { return this.computeInto(helperPlane).isIntersectionLine(l); } }