Skip to content

Commit

Permalink
feat: added css style component
Browse files Browse the repository at this point in the history
  • Loading branch information
tsukinoko-kun committed Jul 22, 2024
1 parent 13bd794 commit 2782c6c
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 49 deletions.
1 change: 1 addition & 0 deletions lib/builtin/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./uiNode"
export * from "./uiStyle"
export * from "./uiText"
4 changes: 1 addition & 3 deletions lib/builtin/components/uiNode.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
import { Component } from "../../component"

export class UiNode extends Component {}
export class UiNode {}
20 changes: 20 additions & 0 deletions lib/builtin/components/uiStyle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export class UiStyle {
public readonly cssObject: CSSStyleDeclaration

constructor() {
this.cssObject = document.createElement("div").style
}

public get css(): string {
return this.cssObject.cssText
}

public set<K extends keyof CSSStyleDeclaration>(key: K, value: CSSStyleDeclaration[K]): this {
this.cssObject[key] = value
return this
}

public get<K extends keyof CSSStyleDeclaration>(key: K): CSSStyleDeclaration[K] {
return this.cssObject[key]
}
}
5 changes: 1 addition & 4 deletions lib/builtin/components/uiText.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Component } from "../../component"

export class UiText extends Component {
export class UiText {
public value: string

constructor(value: string) {
super()
this.value = value
}
}
2 changes: 1 addition & 1 deletion lib/builtin/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ import { renderHtmlRoot } from "../systems"
export function HtmlPlugin(rootSelector: string): Plugin {
return (world) => {
world.insertResource(new HtmlRoot(rootSelector))
world.addSystem(Schedule.Update, renderHtmlRoot)
world.addSystem(Schedule.PostStart, renderHtmlRoot)
}
}
30 changes: 25 additions & 5 deletions lib/builtin/systems/html.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import { res } from "../../resource"
import { HtmlRoot } from "../resources"
import { query } from "../../query"
import { UiText } from "../components"
import { queryRoot } from "../../query"
import { UiStyle, UiText } from "../components"
import { Entity } from "../../entity"
import { Commands } from "../../commands"

export function renderHtmlRoot(): void {
const root = res(HtmlRoot)

let htmlStr = ""
let el = document.createElement("div")

for (const [text] of query(UiText)) {
htmlStr += `<p>${text.value}</p>`
function render(e: Entity, el: HTMLElement): void {
const entitiesHtml = document.createElement("div")
el.appendChild(entitiesHtml)

for (const c of Commands.components(e)) {
if (c instanceof UiText) {
entitiesHtml.innerText += c.value
} else if (c instanceof UiStyle) {
entitiesHtml.style.cssText = c.css
}
}

for (const child of e.children) {
render(child, entitiesHtml)
}
}

for (const [e] of queryRoot([Entity])) {
render(e, el)
}

const htmlStr = el.innerHTML
if (root.root.innerHTML !== htmlStr) {
root.root.innerHTML = htmlStr
}
Expand Down
7 changes: 6 additions & 1 deletion lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,10 @@ export const Commands = {
const world = useWorld()
world.insertResource(resource)
},
trigger(event: Event) {},
trigger(event: Event) {
},
components(entity: Entity): IterableIterator<Component> {
const world = useWorld()
return world.getEntityComponents(entity).values()
},
}
6 changes: 1 addition & 5 deletions lib/component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
export abstract class Component {
public componentId(): string {
return this.constructor.name
}
}
export type Component = Object
4 changes: 4 additions & 0 deletions lib/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export class Entity {
children(new ChildBuilder(this))
return this
}

public toString(): string {
return `Entity::${this.id}`
}
}

export class ChildBuilder {
Expand Down
23 changes: 23 additions & 0 deletions lib/identify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type Ident = symbol

const __identifier__ = Symbol("__identifier__")

export function identify<T extends Object>(o: T | (new (...arg: any[]) => T)): Ident {
if (o instanceof Function) {
return Symbol.for(String(o))
}
if (__identifier__ in o) {
return o[__identifier__] as Ident
}

if (o.constructor.name !== "Object") {
// @ts-ignore
return (o[__identifier__] = Symbol.for(String(o.constructor)))
} else {
console.warn("please use classes for components and resources")

// all objects are considered the same use content to differentiate
// @ts-ignore
return (o[__identifier__] = Symbol.for(JSON.stringify(Object.keys(o).sort())))
}
}
89 changes: 74 additions & 15 deletions lib/query.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,66 @@
import { Component } from "./component"
import type { Component } from "./component"
import { useWorld } from "./world"
import { type Ident, identify } from "./identify"

export type QueryRule = (components: IterableIterator<Ident>) => boolean

export function queryAnd(...components: Component[]): QueryRule {
const andComponentIdents = components.map(identify)

return (test: IterableIterator<Ident>) => {
for (const tc of test) {
if (!andComponentIdents.includes(tc)) {
return false
}
}

return true
}
}

// query([UiText], queryAnd(UiText, UiText))

/**
* Get all entities with the specified components (children are included)
*/
export function* query<T extends any[]>(...types: { [K in keyof T]: new (...arg: any[]) => T[K] }): Generator<T> {
export function* query<T extends any[]>(
types: { [K in keyof T]: new (...arg: any[]) => T[K] },
...filter: QueryRule[]
): Generator<T> {
const world = useWorld()

const requiredComponents = new Array<string>(types.length)
// list of component names that are required
const requiredComponents = new Array<Ident>(types.length)

// fill the requiredComponents array with the names of the required components
for (let i = 0; i < types.length; i++) {
const t = types[i]
if (t == undefined) {
if (t == undefined || t.name === "Entity") {
continue
}

if (t.prototype instanceof Component) {
requiredComponents[i] = t.prototype.constructor.name
}
requiredComponents[i] = identify(t)
}

// find all entities with the required components
entity_loop: for (const [e, c] of world.getComponents()) {
for (const component of requiredComponents) {
if (component == undefined) {
continue
}
if (!c.has(component)) {
continue entity_loop
}
}

// apply filters
for (const f of filter) {
if (!f(c.keys())) {
console.log("filter failed")
continue entity_loop
}
}

const x = new Array<any>(types.length) as T
for (let i = 0; i < types.length; i++) {
const t = types[i]
Expand All @@ -35,7 +69,12 @@ export function* query<T extends any[]>(...types: { [K in keyof T]: new (...arg:
continue
}

x[i] = c.get(t.prototype.constructor.name)!
if (t.name === "Entity") {
x[i] = e
continue
}

x[i] = c.get(identify(t))
}

yield x
Expand All @@ -45,30 +84,45 @@ export function* query<T extends any[]>(...types: { [K in keyof T]: new (...arg:
/**
* Same as query, but only queries the root entities (entities without parents)
*/
export function* queryRoot<T extends any[]>(...types: { [K in keyof T]: new (...arg: any[]) => T[K] }): Generator<T> {
export function* queryRoot<T extends any[]>(
types: { [K in keyof T]: new (...arg: any[]) => T[K] },
...filter: QueryRule[]
): Generator<T> {
const world = useWorld()

const requiredComponents = new Array<string>(types.length)
// list of component names that are required
const requiredComponents = new Array<Ident>(types.length)

// fill the requiredComponents array with the names of the required components
for (let i = 0; i < types.length; i++) {
const t = types[i]
if (t == undefined) {
if (t == undefined || t.name === "Entity") {
continue
}

if (t.prototype instanceof Component) {
requiredComponents[i] = t.prototype.constructor.name
}
requiredComponents[i] = identify(t)
}

// find all entities with the required components
entity_loop: for (const e of world.getEntities()) {
const c = world.getEntityComponents(e)
for (const component of requiredComponents) {
if (component == undefined) {
continue
}
if (!c.has(component)) {
continue entity_loop
}
}

// apply filters
for (const f of filter) {
if (!f(c.keys())) {
console.log("filter failed")
continue entity_loop
}
}

const x = new Array<any>(types.length) as T
for (let i = 0; i < types.length; i++) {
const t = types[i]
Expand All @@ -77,7 +131,12 @@ export function* queryRoot<T extends any[]>(...types: { [K in keyof T]: new (...
continue
}

x[i] = c.get(t.prototype.constructor.name)!
if (t.name === "Entity") {
x[i] = e
continue
}

x[i] = c.get(identify(t))
}

yield x
Expand Down
26 changes: 13 additions & 13 deletions lib/world.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Entity } from "./entity"
import type { Component } from "./component"
import { type Component } from "./component"
import type { System } from "./system"
import { Schedule } from "./schedule"
import { type Ident, identify } from "./identify"

let currentWorld: World | null = null

Expand All @@ -19,9 +20,9 @@ export function useWorld(): World {

export class World {
private readonly entities = new Array<Entity>()
private readonly components = new Map<Entity, Map<string, Component>>()
private readonly components = new Map<Entity, Map<Ident, Component>>()
private readonly systems = new Map<Schedule, Array<System>>()
private readonly resources = new Map<string, Object>()
private readonly resources = new Map<Ident, Object>()

public getSystems(): ReadonlyMap<Schedule, ReadonlyArray<System>> {
return this.systems
Expand All @@ -40,25 +41,24 @@ export class World {
return this.entities
}

public getComponents(): ReadonlyMap<Entity, ReadonlyMap<string, Component>> {
public getComponents(): ReadonlyMap<Entity, ReadonlyMap<Ident, Component>> {
return this.components
}

public getEntityComponents(e: Entity): ReadonlyMap<string, Component> {
public getEntityComponents(e: Entity): ReadonlyMap<Ident, Component> {
if (!this.components.has(e)) {
throw new Error(`Entity ${e} does not exist in the world`)
}
return this.components.get(e) ?? new Map()
}

public getResourceSafe<T extends Object>(t: { new (...args: any[]): T }): T | undefined {
return this.resources.get(t.name) as T | undefined
return this.resources.get(identify(t)) as T | undefined
}

public getResource<T>(t: { new (...args: any[]): T }): T {
const r = this.resources.get(t.name)
const r = this.resources.get(identify(t))
if (!r) {
console.debug(this.resources)
throw new Error(`Resource ${t.name} does not exist`)
}
return r as T
Expand Down Expand Up @@ -96,7 +96,7 @@ export class World {
if (!this.components.has(e)) {
this.components.set(e, new Map())
}
this.components.get(e)!.set(component.componentId(), component)
this.components.get(e)!.set(identify(component), component)
}

public addComponents(e: Entity, ...components: Component[]): void {
Expand All @@ -108,15 +108,15 @@ export class World {
this.components.set(e, new Map())
}
for (const component of components) {
this.components.get(e)!.set(component.componentId(), component)
this.components.get(e)!.set(identify(component), component)
}
}

public spawn(...components: Component[]): Entity {
const e = this.spawnEmpty()
const entityComponents = new Map<string, Component>()
const entityComponents = new Map<Ident, Component>()
for (const component of components) {
entityComponents.set(component.componentId(), component)
entityComponents.set(identify(component), component)
}
this.components.set(e, entityComponents)
return e
Expand All @@ -130,6 +130,6 @@ export class World {
}

public insertResource<T extends Object>(r: T): void {
this.resources.set(r.constructor.name, r)
this.resources.set(identify(r), r)
}
}
Loading

0 comments on commit 2782c6c

Please sign in to comment.