From 1759e89b3b290508378a739d4c2ddd6f7b5d9673 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Sun, 5 Jul 2026 17:18:11 -0400 Subject: [PATCH] unify timeline, world. shadeless schematics. --- engine.js | 14 +++- index.html | 236 +++++++++++++---------------------------------------- world.js | 201 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 265 insertions(+), 186 deletions(-) diff --git a/engine.js b/engine.js index db2903f3..8cac0b69 100644 --- a/engine.js +++ b/engine.js @@ -17,6 +17,7 @@ layout(location=9) in vec3 i_color; uniform mat4 u_viewProj; uniform vec3 u_lightDir; uniform vec3 u_viewDir; +uniform vec3 u_upDir; out vec2 v_uv; out float v_light; @@ -32,6 +33,15 @@ void main() { float head = max(dot(normal, normalize(u_viewDir)), 0.0); float baseLight = clamp(sun + head * 0.3, 0.0, 1.0) * 0.6 + 0.4; + // Strict Schematic Detection: Both vectors must be perfectly axis-aligned + vec3 absV = abs(u_viewDir); + vec3 absU = abs(u_upDir); + bool isSchematic = (absV.x + absV.y + absV.z == 1.0) && (absU.x + absU.y + absU.z == 1.0); + + if (isSchematic && head == 1.0) { + baseLight = 1.0; + } + v_light = mix(1.0, baseLight, a_shade); v_color = mix(vec3(1.0), i_color, a_tint); } @@ -81,6 +91,7 @@ export class Engine { viewProj: gl.getUniformLocation(this.program, "u_viewProj"), lightDir: gl.getUniformLocation(this.program, "u_lightDir"), viewDir: gl.getUniformLocation(this.program, "u_viewDir"), + upDir: gl.getUniformLocation(this.program, "u_upDir"), texture: gl.getUniformLocation(this.program, "u_texture") }; @@ -306,8 +317,9 @@ export class Engine { const viewProj = diorama.updateMatrices(aspect); gl.uniformMatrix4fv(this.uniforms.viewProj, false, viewProj); - gl.uniform3f(this.uniforms.lightDir, 1.0, 3.0, 2.0); + gl.uniform3f(this.uniforms.lightDir, 1.0, 3.0, -2.0); gl.uniform3f(this.uniforms.viewDir, diorama.viewDir[0], diorama.viewDir[1], diorama.viewDir[2]); + gl.uniform3f(this.uniforms.upDir, diorama.upDir[0], diorama.upDir[1], diorama.upDir[2]); const meshes = this.worldMeshes.get(diorama.world) || []; for (const mesh of meshes) { diff --git a/index.html b/index.html index f8558483..d9471e9e 100644 --- a/index.html +++ b/index.html @@ -32,95 +32,16 @@

Visualizer

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper ut mauris sed - tempor. Nunc - vehicula tempor purus, non vestibulum libero ornare non. Morbi fringilla sapien diam, sed - rhoncus lacus egestas - vel. -

- -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper ut mauris sed - tempor. Nunc - vehicula tempor purus, non vestibulum libero ornare non. Morbi fringilla sapien diam, sed - rhoncus lacus egestas - vel. -

+

Static

-

- Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis - aliquam tellus, - vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar - ligula eu maximus. - Mauris ullamcorper accumsan dui vel pulvinar. -

- -

- Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis - aliquam tellus, - vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar - ligula eu maximus. - Mauris ullamcorper accumsan dui vel pulvinar. -

- -

- Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis - aliquam tellus, - vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar - ligula eu maximus. - Mauris ullamcorper accumsan dui vel pulvinar. -

- -

- Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis - aliquam tellus, - vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar - ligula eu maximus. - Mauris ullamcorper accumsan dui vel pulvinar. -

- -

- Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis - aliquam tellus, - vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar - ligula eu maximus. - Mauris ullamcorper accumsan dui vel pulvinar. -

- -

- Curabitur vestibulum vitae orci ac laoreet. Donec vel imperdiet tortor. Vestibulum lobortis - aliquam tellus, - vitae viverra nisi porttitor quis. Pellentesque id efficitur arcu. Nunc laoreet pulvinar - ligula eu maximus. - Mauris ullamcorper accumsan dui vel pulvinar. -

- +

Dynamic

-

- Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque - penatibus et magnis - dis parturient montes, nascetur ridiculus mus. -

- -

- Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque - penatibus et magnis - dis parturient montes, nascetur ridiculus mus. -

- -

- Curabitur tincidunt sapien et diam fringilla, eu accumsan odio faucibus. Orci varius natoque - penatibus et magnis - dis parturient montes, nascetur ridiculus mus. -

-
@@ -135,61 +56,50 @@ const engine = new Engine(); document.getElementById('atlas-container')?.appendChild(engine.atlas.canvas); - const world1 = new World(); - world1.set('minecraft:redstone_wire', -1, 0, 0, { - east: 'side', - west: 'none', - north: 'none', - south: 'none', - power: '15' - }); - world1.set('minecraft:repeater', 0, 0, 0, { - facing: 'east', - delay: '1', - locked: 'false', - powered: 'true' - }); - world1.set('minecraft:oak_trapdoor', 2, 0, 0, { - facing: 'east', - half: 'bottom', - open: 'true' - }); - world1.set('lodestone', 0, -1, 0); - world1.set('piston', 0, -2, 0, {facing: 'north', extended: 'false'}); + const world1 = new World(` + p -1 0 0 redstone_wire east=side west=none north=none south=none power=15 + p 0 0 0 repeater facing=east delay=1 locked=false powered=true + p 2 0 0 oak_trapdoor facing=east half=bottom open=true + p 0 -1 0 lodestone + p 0 -2 0 piston facing=north extended=false + `) - const world2 = new World(); - world2.set('minecraft:redstone_wire', 0, 0, 0, { - east: 'side', - west: 'none', - north: 'none', - south: 'none', - power: '0' - }); - world2.set('minecraft:repeater', 1, 0, 0, { - facing: 'east', - delay: '4', - locked: 'false', - powered: 'false' - }); - world2.set('minecraft:sticky_piston', 2, 0, 0, {facing: 'east', extended: 'false'}); - world2.set('minecraft:sticky_piston', 2, 0, 1, {facing: 'east', extended: 'true'}); - world2.set('minecraft:sticky_piston', 2, 0, 2, {facing: 'east', extended: 'true'}); - world2.set('minecraft:piston_head', 2.5, 0, 1, { - facing: 'east', - short: 'true', - type: 'sticky' - }); - world2.set('minecraft:piston_head', 3, 0, 2, { - facing: 'east', - short: 'false', - type: 'sticky' - }); - world2.set('minecraft:glass', 3, 0, 0); - world2.set('minecraft:observer', 3.5, 0, 1, {facing: 'up', powered: 'true'}); - world2.set('minecraft:redstone_block', 4, 0, 2); + const world2 = new World(` + p 0 0 0 redstone_wire power=0 east=side west=none north=none south=none + p 1 0 0 repeater facing=west powered=false locked=false delay=2 + p 2 0 0 piston facing=east extended=false + + t 2 + p 0 0 0 power=15 + + t 6 + p 1 0 0 powered=true + + t 7 + p 2 0 0 extended=true + p 2.5 0 0 piston_head facing=east short=true type=normal + + t 8 + p 2.5 0 0 air + p 3 0 0 piston_head facing=east short=false type=normal + + t 12 + p 0 0 0 power=0 + + t 16 + p 1 0 0 powered=false + + t 17 + p 3 0 0 air + p 2.5 0 0 piston_head facing=east short=true type=normal + + t 18 + p 2.5 0 0 air + p 2 0 0 extended=false + `); const demo1 = new Diorama('demo-1', world1, () => engine.requestRender()) - demo1.radius = 2; + demo1.radius = 3; demo1.phi = 10; demo1.theta = -5; demo1.target = [0.5, 0, 0.5] @@ -197,7 +107,7 @@ engine.addDiorama(demo1); const demo2 = new Diorama('demo-2', world2, () => engine.requestRender()) - demo2.radius = 4; + demo2.radius = 5; demo2.theta = 180; demo2.phi = 90; demo2.centerView() @@ -205,9 +115,7 @@ engine.addDiorama(demo2); const demo3 = new Diorama('demo-3', world2, () => engine.requestRender()) - demo3.radius = 4; - demo3.theta = 0; - demo3.phi = 0; + demo3.radius = 5; demo3.centerView() demo3.saveState() engine.addDiorama(demo3); @@ -215,51 +123,21 @@ // Fetch and build everything concurrently! await engine.updateAll(); - function set0() { - world2.set('minecraft:sticky_piston', 2, 0, 2, {facing: 'east', extended: 'false'}); - world2.set('minecraft:redstone_block', 3, 0, 2); - world2.del(2.5, 0, 2); - world2.del(3.5, 0, 2); - setTimeout(set1, 250) - } + const factor = 1; + setInterval(() => { + for (let i = 0; i < 20; i++) { + setTimeout(() => world2.seek(i), i * factor * 50) + } + }, factor * 1000) - function set1() { - world2.set('minecraft:sticky_piston', 2, 0, 2, {facing: 'east', extended: 'true'}); - world2.del(3, 0, 2) - world2.set('minecraft:piston_head', 2.5, 0, 2, { - facing: 'east', - short: 'true', - type: 'sticky' - }); - world2.set('minecraft:redstone_block', 3.5, 0, 2); - setTimeout(set2, 250) - } + // let i = 0; + // let n = world2.eventCount; + // setInterval(() => { + // world2.seekIndex(i); + // i = (i + 1) % n; + // }, 500); - function set2() { - world2.set('minecraft:piston_head', 3, 0, 2, { - facing: 'east', - short: 'false', - type: 'sticky' - }); - world2.del(2.5, 0, 2); - world2.del(3.5, 0, 2); - world2.set('minecraft:redstone_block', 4, 0, 2); - setTimeout(set3, 250) - } - function set3() { - world2.del(4, 0, 2) - world2.del(3, 0, 2) - world2.set('minecraft:piston_head', 2.5, 0, 2, { - facing: 'east', - short: 'true', - type: 'sticky' - }); - world2.set('minecraft:redstone_block', 3.5, 0, 2); - setTimeout(set0, 250) - } - - setTimeout(set3, 1000) } catch (err) { console.error("Renderer Initialization Failed:", err); } diff --git a/world.js b/world.js index 6255d580..51a46fdc 100644 --- a/world.js +++ b/world.js @@ -7,9 +7,25 @@ export class Block { } export class World { - constructor() { + constructor(script = '') { this.blocks = new Map(); this.listeners = new Set(); + + // Timeline State + this.events = []; + this.labels = new Map(); + + this.currentIndex = 0; + this.currentTime = -Infinity; + + this.initialState = new Map(); + this.initialIndex = 0; + + this.eventCount = 0; + + if (script) { + this.#compile(script); + } } subscribe(callback) { @@ -23,12 +39,14 @@ export class World { set(id, x, y, z, state = {}) { const key = `${x},${y},${z}`; - this.blocks.set(key, new Block(id, [x, y, z], state)); - this.notify(); - } + const isAir = id === 'air' || id === 'minecraft:air'; - del(x, y, z) { - if (this.blocks.delete(`${x},${y},${z}`)) { + if (isAir) { + if (this.blocks.delete(key)) { + this.notify(); + } + } else { + this.blocks.set(key, new Block(id, [x, y, z], state)); this.notify(); } } @@ -36,4 +54,175 @@ export class World { get(x, y, z) { return this.blocks.get(`${x},${y},${z}`); } + + #compile(script) { + let currentTime = -1; + let subTick = 0; + let capturedInitialState = false; + + const shadowWorld = new Map(); + + const captureInitial = () => { + if (capturedInitialState) return; + for (const [k, v] of shadowWorld.entries()) { + this.initialState.set(k, {id: v.id, pos: [...v.pos], state: {...v.state}}); + } + this.initialIndex = this.events.length; + capturedInitialState = true; + }; + + const lines = script.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')); + + for (const line of lines) { + const tokens = line.split(/\s+/); + const cmd = tokens[0]; + + if (cmd === 't') { + const newTime = parseFloat(tokens[1]); + + // If we are crossing the tick 0 threshold, lock in the initial state + if (newTime >= 0 && !capturedInitialState) captureInitial(); + + currentTime = newTime; + subTick = 0; + continue; + } + + if (cmd === 'l') { + this.labels.set(tokens[1], currentTime); + continue; + } + + if (cmd === 'p') { + const x = parseFloat(tokens[1]); + const y = parseFloat(tokens[2]); + const z = parseFloat(tokens[3]); + const key = `${x},${y},${z}`; + + let id = null; + const newState = {}; + + for (let i = 4; i < tokens.length; i++) { + const token = tokens[i]; + if (token.includes('=')) { + const [k, v] = token.split('='); + newState[k] = v; + } else { + id = token; + } + } + + const prevBlock = shadowWorld.get(key); + const prev = prevBlock ? {id: prevBlock.id, state: {...prevBlock.state}} : null; + + const isAir = id === 'air' || id === 'minecraft:air'; + + if (isAir) { + shadowWorld.delete(key); + } else if (id) { + shadowWorld.set(key, {id, pos: [x, y, z], state: newState}); + } else if (prevBlock) { + Object.assign(prevBlock.state, newState); + } else { + console.warn(`Timeline warning: Attempted to update state of non-existent block at ${key}`); + continue; + } + + const nextBlock = shadowWorld.get(key); + const next = nextBlock ? {id: nextBlock.id, state: {...nextBlock.state}} : null; + + this.events.push({ + time: currentTime, + subTick: subTick++, + x, y, z, + prev, + next + }); + } + } + + // Fallback catch if the script ends entirely before tick 0 + if (!capturedInitialState) captureInitial(); + + this.eventCount = this.events.length - this.initialIndex; + + this.reset(); + } + + #apply(data, x, y, z) { + const key = `${x},${y},${z}`; + if (data) { + this.blocks.set(key, new Block(data.id, [x, y, z], {...data.state})); + } else { + this.blocks.delete(key); + } + } + + seek(target) { + let targetTime = target; + + if (typeof target === 'string') { + targetTime = this.labels.get(target); + if (targetTime === undefined) throw new Error(`Label not found: ${target}`); + } + + // Scrub Forward + while (this.currentIndex < this.events.length && this.events[this.currentIndex].time <= targetTime) { + const ev = this.events[this.currentIndex]; + this.#apply(ev.next, ev.x, ev.y, ev.z); + this.currentIndex++; + } + + // Scrub Backward + while (this.currentIndex > 0 && this.events[this.currentIndex - 1].time > targetTime) { + this.currentIndex--; + const ev = this.events[this.currentIndex]; + this.#apply(ev.prev, ev.x, ev.y, ev.z); + } + + this.currentTime = targetTime; + this.notify(); // Dispatch a single batched update to the Engine + } + + seekIndex(targetIndex) { + targetIndex = targetIndex + this.initialIndex; + + while (this.currentIndex < this.events.length && this.currentIndex < targetIndex) { + const ev = this.events[this.currentIndex]; + this.#apply(ev.next, ev.x, ev.y, ev.z); + this.currentIndex++; + } + + while (this.currentIndex > 0 && this.currentIndex > targetIndex) { + this.currentIndex--; + const ev = this.events[this.currentIndex]; + this.#apply(ev.prev, ev.x, ev.y, ev.z); + } + + if (this.currentIndex < this.events.length) { + this.currentTime = this.events[this.currentIndex].time; + } else if (this.events.length > 0) { + this.currentTime = this.events[this.events.length - 1].time; + } + + this.notify(); + } + + reset() { + this.blocks.clear(); + + for (const [key, block] of this.initialState.entries()) { + this.blocks.set(key, new Block(block.id, [...block.pos], {...block.state})); + } + + this.currentIndex = this.initialIndex; + + if (this.initialIndex > 0 && this.events.length > 0) { + this.currentTime = this.events[this.initialIndex - 1].time; + } else { + this.currentTime = -Infinity; + } + + this.notify(); + } } \ No newline at end of file