UNPKG

@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
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 { @serializable() left: number = 0; @serializable() right: number = 0; @serializable() top: number = 0; @serializable() 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(); // } @serializable() childAlignment: TextAnchor = TextAnchor.UpperLeft; @serializable() reverseArrangement: boolean = false; @serializable() spacing: number = 0; @serializable(Padding) padding!: Padding; @serializable() minWidth: number = 0; @serializable() minHeight: number = 0; @serializable() flexibleHeight: number = 0; @serializable() flexibleWidth: number = 0; @serializable() preferredHeight: number = 0; @serializable() 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 { @serializable() childControlHeight: boolean = true; @serializable() childControlWidth: boolean = true; @serializable() childForceExpandHeight: boolean = false; @serializable() childForceExpandWidth: boolean = false; @serializable() childScaleHeight: boolean = false; @serializable() 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() { } }