@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
335 lines (282 loc) • 10.6 kB
text/typescript
import { serializable } from "../../engine/engine_serialization.js";
import { getParam } from "../../engine/engine_utils.js";
import { Behaviour, GameObject } from "../Component.js";
import { Canvas } from "./Canvas.js";
import { type ILayoutGroup, type IRectTransform } from "./Interfaces.js";
import { RectTransform } from "./RectTransform.js";
const debug = getParam("debuguilayout");
export class Padding {
left: number = 0;
right: number = 0;
top: number = 0;
bottom: number = 0;
get vertical() {
return this.top + this.bottom;
}
get horizontal() {
return this.left + this.right;
}
}
export enum TextAnchor {
UpperLeft = 0,
UpperCenter = 1,
UpperRight = 2,
MiddleLeft = 3,
MiddleCenter = 4,
MiddleRight = 5,
LowerLeft = 6,
LowerCenter = 7,
LowerRight = 8,
Custom = 9
}
enum Axis {
Horizontal = "x",
Vertical = "y"
}
export abstract class LayoutGroup extends Behaviour implements ILayoutGroup {
private _rectTransform: RectTransform | null = null;
private get rectTransform() {
return this._rectTransform;
}
onParentRectTransformChanged(_comp: IRectTransform): void {
this._needsUpdate = true;
}
private _needsUpdate: boolean = false;
get isDirty(): boolean {
return this._needsUpdate;
}
get isLayoutGroup(): boolean {
return true;
}
updateLayout() {
if (!this._rectTransform) return;
if (debug)
console.warn("Layout Update", this.context.time.frame, this.name);
this._needsUpdate = false;
this.onCalculateLayout(this._rectTransform);
}
// onBeforeRender(): void {
// this.updateLayout();
// }
childAlignment: TextAnchor = TextAnchor.UpperLeft;
reverseArrangement: boolean = false;
spacing: number = 0;
padding!: Padding;
minWidth: number = 0;
minHeight: number = 0;
flexibleHeight: number = 0;
flexibleWidth: number = 0;
preferredHeight: number = 0;
preferredWidth: number = 0;
start() {
this._needsUpdate = true;
}
onEnable(): void {
if(debug) console.log(this.name, this);
this._rectTransform = this.gameObject.getComponent(RectTransform);
const canvas = this.gameObject.getComponentInParent(Canvas);
if (canvas) {
canvas.registerLayoutGroup(this);
}
this._needsUpdate = true;
}
onDisable(): void {
const canvas = this.gameObject.getComponentInParent(Canvas);
if (canvas) {
canvas.unregisterLayoutGroup(this);
}
}
protected abstract onCalculateLayout(rt: RectTransform);
// for animation:
private set m_Spacing(val) {
if (val === this.spacing) return;
this._needsUpdate = true;
this.spacing = val;
}
get m_Spacing() {
return this.spacing;
}
}
export abstract class HorizontalOrVerticalLayoutGroup extends LayoutGroup {
childControlHeight: boolean = true;
childControlWidth: boolean = true;
childForceExpandHeight: boolean = false;
childForceExpandWidth: boolean = false;
childScaleHeight: boolean = false;
childScaleWidth: boolean = false;
protected abstract get primaryAxis(): Axis;
protected onCalculateLayout(rect: RectTransform) {
const axis = this.primaryAxis;
const totalWidth = rect.width;
let actualWidth = totalWidth;
const totalHeight = rect.height;
let actualHeight = totalHeight;
actualWidth -= this.padding.horizontal;
actualHeight -= this.padding.vertical;
// if (rect.name === "Title")
// console.log(rect.name, "width=" + totalWidth + ", height=" + totalHeight, rect.anchoredPosition.x)
const paddingAxis = axis === Axis.Horizontal ? this.padding.horizontal : this.padding.vertical;
const isHorizontal = axis === Axis.Horizontal;
const isVertical = !isHorizontal;
const otherAxis = isHorizontal ? "y" : "x";
const controlSize = isHorizontal ? this.childControlWidth : this.childControlHeight;
const controlSizeOtherAxis = isHorizontal ? this.childControlHeight : this.childControlWidth;
const forceExpandSize = isHorizontal ? this.childForceExpandWidth : this.childForceExpandHeight;
const forceExpandSizeOtherAxis = isHorizontal ? this.childForceExpandHeight : this.childForceExpandWidth;
const actualExpandSize = isHorizontal ? actualHeight : actualWidth;
const totalSpace = isHorizontal ? totalWidth : totalHeight;
// 0 is left/top, 0.5 is middle, 1 is right/bottom
const alignmentOnAxis = 0.5 * (isHorizontal ? this.childAlignment % 3 : Math.floor(this.childAlignment / 3));
let start = 0;
if (isHorizontal) {
start += this.padding.left;
}
else
start += this.padding.top;
// Calculate total size of the elements
let totalChildSize = 0;
let actualRectTransformChildCount = 0;
for (let i = 0; i < this.gameObject.children.length; i++) {
const ch = this.gameObject.children[i];
const rt = GameObject.getComponent(ch, RectTransform);
if (rt?.activeAndEnabled) {
actualRectTransformChildCount += 1;
if (isHorizontal) {
totalChildSize += rt.width;
}
else {
totalChildSize += rt.height;
}
}
}
let sizePerChild = 0;
const totalSpacing = this.spacing * (actualRectTransformChildCount - 1)
if (forceExpandSize || controlSize) {
let size = 0;
if (isHorizontal) {
size = actualWidth -= totalSpacing;
}
else {
size = actualHeight -= totalSpacing;
}
if (actualRectTransformChildCount > 0)
sizePerChild = size / actualRectTransformChildCount;
}
let leftOffset = 0;
leftOffset += this.padding.left;
leftOffset -= this.padding.right;
if (alignmentOnAxis !== 0) {
start = totalSpace - totalChildSize;
start *= alignmentOnAxis;
start -= totalSpacing * alignmentOnAxis;
if (isHorizontal) {
start -= this.padding.right * alignmentOnAxis;
start += this.padding.left * (1 - alignmentOnAxis);
if (start < this.padding.left) {
start = this.padding.left;
}
}
else {
start -= this.padding.bottom * alignmentOnAxis;
start += this.padding.top * (1 - alignmentOnAxis);
if (start < this.padding.top) {
start = this.padding.top;
}
}
}
// Apply layout
let k = 1;
for (let i = 0; i < this.gameObject.children.length; i++) {
const ch = this.gameObject.children[i];
const rt = GameObject.getComponent(ch, RectTransform);
if (rt?.activeAndEnabled) {
rt.pivot?.set(.5, .5);
rt.anchorMin.set(0, 1);
rt.anchorMax.set(0, 1);
// Horizontal padding
const x = totalWidth * .5 + leftOffset * .5;
if (rt.anchoredPosition.x !== x)
rt.anchoredPosition.x = x;
const y = totalHeight * -.5
if (rt.anchoredPosition.y !== y)
rt.anchoredPosition.y = y;
// Set the size for the secondary axis (e.g. height for a horizontal layout group)
if (forceExpandSizeOtherAxis && controlSizeOtherAxis && rt.sizeDelta[otherAxis] !== actualExpandSize) {
rt.sizeDelta[otherAxis] = actualExpandSize;
}
// Set the size for the primary axis (e.g. width for a horizontal layout group)
if (forceExpandSize && controlSize && rt.sizeDelta[axis] !== sizePerChild) {
rt.sizeDelta[axis] = sizePerChild
}
const size = isHorizontal ? rt.width : rt.height;
const halfSize = size * .5;
start += halfSize;
// TODO: this isnt correct yet!
if (forceExpandSize) {
// this is the center of the cell
const preferredStart = sizePerChild * k - sizePerChild * .5;
if (preferredStart > start) {
start = preferredStart - sizePerChild * .5 + size + this.padding.left;
start -= halfSize;
}
}
let value = start;
if (axis === Axis.Vertical)
value = -value;
// Only set the position if it's not already the correct one to avoid triggering the rectTransform dirty event
if (rt.anchoredPosition[axis] !== value) {
rt.anchoredPosition[axis] = value
}
start += halfSize;
start += this.spacing;
k += 1;
}
}
}
}
/**
* @category User Interface
* @group Components
*/
export class VerticalLayoutGroup extends HorizontalOrVerticalLayoutGroup {
protected get primaryAxis() {
return Axis.Vertical;
}
}
/**
* @category User Interface
* @group Components
*/
export class HorizontalLayoutGroup extends HorizontalOrVerticalLayoutGroup {
protected get primaryAxis() {
return Axis.Horizontal;
}
}
/**
* @category User Interface
* @group Components
*/
export class GridLayoutGroup extends LayoutGroup {
protected onCalculateLayout() {
}
}