Skip to content

Commit

Permalink
First steps towards a region editor!
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremypenner committed Feb 4, 2024
1 parent 50a082b commit baf5402
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 17 deletions.
59 changes: 59 additions & 0 deletions inspector/edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>Habitat Inspector</title>
<link rel="stylesheet" href="style.css">
<script type="importmap">
{
"imports": {
"htm": "./vendor/htm.mjs",
"preact": "./vendor/preact.mjs",
"preact/hooks": "./vendor/hooks.mjs",
"@preact/signals-core": "./vendor/signals-core.mjs",
"@preact/signals": "./vendor/signals.mjs"
}
}
</script>
<script type="module">
// import "https://esm.sh/*preact/devtools"
import { render } from "preact"
import { signal } from "@preact/signals"
import { html, errors } from "./view.js"
import { useHabitatJson, errorBucket, until } from "./data.js"
import { regionView } from "./region.js"
import { navigationView } from "./navigate.js"
import { selectionInteraction, propEditor, createEditTracker } from "./edit.js"
import { jsonDump } from "./show.js"

const q = (k) => (new URLSearchParams(window.location.search)).get(k)
const filename = q("f") ?? "db/new_Downtown/Downtown_4d.json"

const tracker = createEditTracker()
const objectList = signal(
(await until(() => useHabitatJson(filename), o => o.length > 0))
.map(o => tracker.trackSignal(signal(o))))

const regionName = ({ objects }) => {
const region = objects.find(obj => obj.type == "context")
return html`<span>${(region && region.name) ?? filename}</span>`
}

const regionPage = (_) => html`
<h1>Region Editor - <${regionName} objects=${objectList.value}/></h1>
<div style="display: flex; flex-wrap: wrap">
<${regionView} objects=${objectList.value} interaction=${selectionInteraction}/>
</div>
<${propEditor} objects=${objectList.value}/>
<${jsonDump} heading="JSON" value=${objectList.value}/>
<${errors}/>`

render(html`<${regionPage}/>`, document.getElementById("regionview"))
</script>
</head>
<body>
<div id="regionview">
</div>
<a href="index.html">Home</a>
</body>
</html>
154 changes: 154 additions & 0 deletions inspector/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { createContext } from "preact"
import { useContext } from "preact/hooks"
import { signal, effect } from "@preact/signals"
import { html } from "./view.js"
import { emptyBitmap } from "./codec.js"
import { c64Colors, canvasFromBitmap, canvasImage } from "./render.js"
import { colorsFromOrientation } from "./neohabitat.js"

export const Selection = createContext(signal(null))
export const selectionInteraction = ({ object, children }) => {
const selectionRef = useContext(Selection)
return html`
<div onclick=${() => selectionRef.value = object.ref}>
${selectionRef.value === object.ref ? html`
<div style="position: absolute; left: 0; right: 0; top: 0; bottom: 0;
background-color: #ff000040; border: 1px solid red;"></div>
` : null}
${children}
</a>`
}

export const createEditTracker = () => {
const editHistory = []
const redoHistory = []

const update = (obj, key, value) => {
const result = Array.isArray(obj) ? [...obj] : {...obj}
result[key] = value
return result
}

const updateIn = (obj, place, key, value) => {
if (place.length == 0) {
return update(obj, key, value)
} else {
return update(obj, place[0], updateIn(obj[place[0]], place.slice(1), key, value))
}
}

const valueAt = (sig, place) => {
let value = sig.value
for (const placeKey of place) {
value = value[placeKey]
}
return value
}

const performEdit = (sig, place, key, value, history) => {
const previous = valueAt(sig, place)[key]
sig.value = updateIn(sig.value, place, key, value)
history.push({ sig, place, key, value, previous })
}

const change = (sig, place, key, value) => {
performEdit(sig, place, key, value, editHistory)
redoHistory.length = 0
}

const undo = (fromHistory = editHistory, toHistory = redoHistory) => {
const edit = fromHistory.pop()
if (edit) {
performEdit(edit.sig, edit.place, edit.key, edit.previous, toHistory)
}
}

const redo = () => undo(redoHistory, editHistory)
const dynamicProxy = (targetGetter, handler) => {
return new Proxy(targetGetter(), {
ownKeys(target) { return Reflect.ownKeys(targetGetter(target)) },
getOwnPropertyDescriptor(target, prop) { return Reflect.getOwnPropertyDescriptor(targetGetter(target), prop) },
...handler
})
}
const wrapInnerValue = (sig, value, place, refreshParent = () => {}) => {
if (typeof(value) === "object") {
let innerTarget = value
const refresh = () => { innerTarget = valueAt(sig, place) }
return dynamicProxy(() => innerTarget, {
get(target, property, receiver) {
return wrapInnerValue(sig, innerTarget[property], [...place, property], refresh)
},
set(target, property, newValue, receiver) {
change(sig, place, property, newValue)
refresh()
}
})
} else {
return value
}
}

return {
undo,
redo,
editHistory,
redoHistory,
trackSignal(sig) {
return dynamicProxy(() => sig.value, {
get(target, property, receiver) {
return wrapInnerValue(sig, sig.value[property], [property])
},
set(target, property, newValue, receiver) {
change(sig, [], property, newValue)
}
})
}
}
}

export const orientationEditor = ({ obj }) => {
const mod = obj.mods[0]
// color, pattern, flip
const colors = colorsFromOrientation(mod.orientation)
const flipped = (mod.orientation & 0x01) != 0
return html`
<fieldset>
<legend>Orientation</legend>
<div style="display: flex">
${c64Colors.map((color, icolor) => html`
<div key=${`color${icolor}`}
style="width: 48px; height: 48px; margin: 2px;
border: 4px dotted ${colors.wildcard === icolor ? " black" : "transparent"};"
onclick=${() => { mod.orientation = (mod.orientation & 0x07) | 0x80 | (icolor << 3) }}>
<div style="background-color: #${color.toString(16).padStart(6, "0")}; width: 100%; height: 100%;"/>
</div>`)}
</div>
<div style="display: flex">
${[...Array(15).keys()].map((ipattern) => html`
<div key=${`pattern${ipattern}`}
style="width: 48px; height: 48px; margin: 2px;
border: 4px dotted ${colors.pattern === ipattern ? " black" : "transparent"};"
onclick=${() => { mod.orientation = (mod.orientation & 0x07) | (ipattern << 3) }}>
<${canvasImage} canvas=${canvasFromBitmap(emptyBitmap(2, 16, 1), { pattern: ipattern })}/>
</div>`)}
</div>
<div>
<label>
<input type="checkbox" checked=${flipped}
onclick=${() => { mod.orientation = (mod.orientation & 0xfe) | (flipped ? 0 : 1) }}/>
Flip horizontally
</label>
</div>
</fieldset>`
}

export const propEditor = ({ objects }) => {
const selectionRef = useContext(Selection)
if (selectionRef.value != null) {
const obj = objects.find(o => o.ref === selectionRef.value)
if (obj) {
return html`<${orientationEditor} obj=${obj}/>`
}
}
}
4 changes: 2 additions & 2 deletions inspector/region.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import { signal, effect } from "@preact/signals"
import { html, errors } from "./view.js"
import { useHabitatJson, errorBucket } from "./data.js"
import { regionView, directionNav, objectNav, objectDetails } from "./region.js"
import { regionView, directionNav, objectNav, objectDetails, navInteraction } from "./region.js"
import { navigationView } from "./navigate.js"

const q = (k) => (new URLSearchParams(window.location.search)).get(k)
Expand Down Expand Up @@ -73,7 +73,7 @@ <h1>Region - <${regionName} filename=${filename.value}/></h1>
<tr><td/><td><${directionNav} filename=${filename.value} position="top"/></td><td/></tr>
<tr>
<td><${directionNav} filename=${filename.value} position="left"/></td>
<td style="padding: 10px;"><${regionView} filename=${filename.value}/></td>
<td style="padding: 10px;"><${regionView} filename=${filename.value} interaction=${navInteraction}/></td>
<td><${directionNav} filename=${filename.value} position="right"/></td>
</tr>
<tr><td/><td><${directionNav} filename=${filename.value} position="bottom"/></td><td/></tr>
Expand Down
36 changes: 24 additions & 12 deletions inspector/region.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { decodeProp } from "./codec.js"
import { html, catcher } from "./view.js"
import { createContext } from "preact"
import { useContext, useMemo } from "preact/hooks"
import { signal } from "@preact/signals"
import { contextMap, betaMud, logError, promiseToSignal, until, useBinary, useHabitatJson, charset } from './data.js'
Expand Down Expand Up @@ -255,11 +256,20 @@ export const containedItemView = ({ object, containerProp, containerMod, contain

return html`
<${positionedInRegion} space=${objectSpaceFromLayout(layout)} z=${layout.z}>
<${animatedDiv} frames=${layout.frames}/>
<${itemInteractionWrapper} object=${object} mod=${mod}>
<${animatedDiv} frames=${layout.frames}/>
<//>
<//>`
}

export const itemInteraction = ({ mod, children }) => {
export const itemInteraction = createContext(({ children }) => children)

export const itemInteractionWrapper = (props) => {
const interactionView = useContext(itemInteraction)
return html`<${interactionView} ...${props}/>`
}

export const navInteraction = ({ mod, children }) => {
const connection = mod.connection && contextMap()[mod.connection]
if (connection) {
return html`<a href="region.html?f=${connection.filename}">${children}</a>`
Expand All @@ -284,7 +294,7 @@ export const regionItemView = ({ object, contents = [] }) => {

const container = html`
<${positionedInRegion} key=${object.ref} space=${objectSpaceFromLayout(layout)} z=${layout.z}>
<${itemInteraction} mod=${mod}>
<${itemInteractionWrapper} object=${object} mod=${mod}>
<${animatedDiv} frames=${layout.frames}/>
<//>
</div>`
Expand Down Expand Up @@ -313,18 +323,20 @@ const sortObjects = (objects) => {
.sort((o1, o2) => o2.mods[0].y - o1.mods[0].y)])
}

export const regionView = ({ filename }) => {
export const regionView = ({ filename, objects, interaction = ({children}) => children }) => {
const scale = useContext(Scale)
const objects = useHabitatJson(filename)
objects = objects ?? useHabitatJson(filename)

return html`
<div style="position: relative; line-height: 0px; width: ${320 * scale}px; height: ${128 * scale}px; overflow: hidden">
${sortObjects(objects).map(([obj, contents]) => html`
<${itemView} key=${obj.ref}
viewer=${regionItemView}
object=${obj}
contents=${contents}/>`)}
</div>`
<${itemInteraction.Provider} value=${interaction}>
<div style="position: relative; line-height: 0px; width: ${320 * scale}px; height: ${128 * scale}px; overflow: hidden">
${sortObjects(objects).map(([obj, contents]) => html`
<${itemView} key=${obj.ref}
viewer=${regionItemView}
object=${obj}
contents=${contents}/>`)}
</div>
<//>`
}

export const generateRegionCanvas = async (filename) => {
Expand Down
10 changes: 7 additions & 3 deletions inspector/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { emptyBitmap, horizontalLine } from "./codec.js"
import { makeCanvas } from "./shim.js"

// C64 RGB values generated from https://www.colodore.com/ with default settings
const c64Colors = [
export const c64Colors = [
0x000000, 0xffffff, 0x813338, 0x75cec8, 0x8e3c97, 0x56ac4d,
0x2e2c9b, 0xedf171, 0x8e5029, 0x553800, 0xc46c71, 0x4a4a4a,
0x7b7b7b, 0xa9ff9f, 0x706deb, 0xb2b2b2
]

// from paint.m:447
const celPatterns = [
export const celPatterns = [
[0x00, 0x00, 0x00, 0x00],
[0xaa, 0xaa, 0xaa, 0xaa],
[0xff, 0xff, 0xff, 0xff],
Expand Down Expand Up @@ -268,7 +268,11 @@ celLayerRenderer.text = (cel, colors, x, y) => {
textColors.wildcard = 6
pattern = 0x55
}
return frameFromText(x, y, colors.bytes, colors.charset, pattern, cel.fineXOffset, textColors)
if (colors.charset) {
return frameFromText(x, y, colors.bytes, colors.charset, pattern, cel.fineXOffset, textColors)
} else {
return null
}
}

celLayerRenderer.trap = (cel, colors, x, y) => {
Expand Down

0 comments on commit baf5402

Please sign in to comment.