@pmndrs/uikit
Version:
Build performant 3D user interfaces with Three.js and yoga.
238 lines (237 loc) • 8.96 kB
JavaScript
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);
}
}