Hustler-Pixelwelt T11: alte helle Oberfläche und Kanvas-Engine entfernt, workers.css gekürzt, Zusicherungen in die neuen Tests übertragen
- gelöscht: components/workers/engine/* (10 Dateien), WorkersStage, WorkersBoard, WorkerCard, WorkerList, WorkersKpis, die neun Engine-Tests samt workersSmoke (211 Tests) und die Helfer engineVm.mjs und fakeCtx.mjs (nur die Engine-Tests brauchten sie) - workers.css (310 -> 60 Zeilen) behält nur Einstellungen-Dialog und Interessenten-Panel; @font-face für Pixelify Sans steht jetzt in hustler/schrift.css - workersComponents: kein innerHTML und kein Kanvas rekursiv unter components/workers/**, jeder Knopf mit type, Links nur aus geprüften Feldern (externe mit noopener), keine Anmeldung in den Bildschirmen, alte Dateien weg und nirgends mehr importiert - workersCss: jede wk-Klasse im Code hat eine Regel, keine Regel ohne Nutzer; workersFont: Pixelify Sans genau einmal in schrift.css - hustlerAssets: Pixelify Sans in schrift.css erlaubt; hustlerDialog: Link-Ziele nur aus safeLink, Knöpfe mit type; hustlerSeite: Laden/Fehler/bereit am selben Einhängepunkt Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4be9750f36
commit
ba007a325a
35 files changed
+277
-6495
No files matched your search
@@ -1,179 +0,0 @@
|
||||
// vm-Sandbox für die Szene des Hustler-Hochhauses (Muster: entwuerfe/smoke.js, nur für Tests).
|
||||
// scene.js wird mit esbuild zu einem IIFE gebündelt und in einem vm-Kontext ohne window/document ausgeführt:
|
||||
// der Kontext kennt nur console, Math, Date und JSON. Canvas, Zeichenkontext und Bildtakt sind Attrappen.
|
||||
|
||||
import vm from 'node:vm'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import esbuild from 'esbuild'
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')
|
||||
const SCENE = path.join(repoRoot, 'src/components/workers/engine/scene.js')
|
||||
const STEP_MS = 1000 / 60
|
||||
|
||||
// Das Bündel ist für alle Sandboxen gleich: einmal bauen
|
||||
let bundle = null
|
||||
function bundleScene() {
|
||||
bundle ??= esbuild
|
||||
.build({ entryPoints: [SCENE], bundle: true, format: 'iife', globalName: 'WKEngine', platform: 'neutral', write: false })
|
||||
.then((result) => result.outputFiles[0].text)
|
||||
return bundle
|
||||
}
|
||||
|
||||
/**
|
||||
* createEngineSandbox({ cssWidth = 960, dpr = 1, record = false })
|
||||
* → { mountTower, engine, canvas, counts, seenTexts, rects, texts, listeners, listenerOptions,
|
||||
* opts(extra), tick(seconds), jump(seconds), rafPending(), setCssWidth(w),
|
||||
* click(cssX, cssY), pointer(type, cssX, cssY, extra), wheel(cssX, cssY, deltaY, { ctrlKey }) }
|
||||
* counts: { fillRect, fillText } zählt nur (es werden keine Aufrufe gespeichert, sonst sprengen tausende Bilder den Speicher)
|
||||
* seenTexts: Set der verschiedenen gezeichneten Texte
|
||||
* rects/texts: nur mit record: true, jedes fillRect { n, x, y, w, h, fill, alpha } und jedes fillText { n, text, x, y, font }
|
||||
* (n = laufende Nummer über beide Listen, damit die Reihenfolge der Aufrufe vergleichbar ist; alpha folgt save/restore)
|
||||
* listeners: Typ -> Liste der Rückrufe am Canvas; listenerOptions: Map Rückruf -> Optionen von addEventListener
|
||||
* tick(s): rückt die Uhr in Schritten von 1000/60 ms vor und führt fällige raf-Rückrufe aus; jump(s) rückt die Uhr ohne Rückrufe
|
||||
* (ein Stillstand); click/pointer/wheel schicken Ereignisse in CSS-Pixeln relativ zur Leinwand (Rand 0, Breite cssWidth, Höhe 4/5)
|
||||
*/
|
||||
export async function createEngineSandbox({ cssWidth = 960, dpr = 1, record = false } = {}) {
|
||||
const code = await bundleScene()
|
||||
|
||||
const counts = { fillRect: 0, fillText: 0 }
|
||||
const seenTexts = new Set()
|
||||
const rects = []
|
||||
const texts = []
|
||||
const listeners = {}
|
||||
const listenerOptions = new Map()
|
||||
const alphaStack = []
|
||||
let seq = 0
|
||||
let width = cssWidth
|
||||
|
||||
const target = { fillStyle: '#000000', font: '10px sans-serif', globalAlpha: 1, textAlign: 'left', textBaseline: 'alphabetic' }
|
||||
const ctx = new Proxy(target, {
|
||||
get(t, prop) {
|
||||
if (typeof prop === 'symbol') return undefined
|
||||
if (prop === 'save') return () => alphaStack.push(t.globalAlpha)
|
||||
if (prop === 'restore') return () => { if (alphaStack.length > 0) t.globalAlpha = alphaStack.pop() }
|
||||
if (prop === 'measureText') return (text) => ({ width: String(text).length * 7 })
|
||||
if (prop === 'fillRect') {
|
||||
return (x, y, w, h) => {
|
||||
counts.fillRect += 1
|
||||
if (record) rects.push({ n: seq++, x, y, w, h, fill: t.fillStyle, alpha: t.globalAlpha })
|
||||
}
|
||||
}
|
||||
if (prop === 'fillText') {
|
||||
return (text, x, y) => {
|
||||
if (typeof text !== 'string') throw new Error('fillText mit Nicht-String: ' + text)
|
||||
counts.fillText += 1
|
||||
seenTexts.add(text)
|
||||
if (record) texts.push({ n: seq++, text, x, y, font: t.font })
|
||||
}
|
||||
}
|
||||
if (prop === 'createLinearGradient' || prop === 'createRadialGradient') return () => ({ addColorStop() {} })
|
||||
if (prop === 'getImageData') return () => ({ data: new Uint8ClampedArray(4) })
|
||||
if (prop in t) return t[prop]
|
||||
return () => {}
|
||||
},
|
||||
set(t, prop, value) {
|
||||
t[prop] = value
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
const canvas = {
|
||||
width: 300,
|
||||
height: 150,
|
||||
style: {},
|
||||
getContext: () => ctx,
|
||||
addEventListener(type, fn, options) {
|
||||
listeners[type] = [...(listeners[type] ?? []), fn]
|
||||
if (options !== undefined) listenerOptions.set(fn, options)
|
||||
},
|
||||
removeEventListener(type, fn) {
|
||||
listeners[type] = (listeners[type] ?? []).filter((f) => f !== fn)
|
||||
},
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width, height: width * 0.8 }),
|
||||
}
|
||||
target.canvas = canvas
|
||||
|
||||
// Bildtakt: raf/caf führen Buch, now ist die Uhr der Sandbox
|
||||
let clock = 0
|
||||
let nextId = 0
|
||||
const queue = new Map()
|
||||
const raf = (cb) => {
|
||||
nextId += 1
|
||||
queue.set(nextId, cb)
|
||||
return nextId
|
||||
}
|
||||
const caf = (id) => {
|
||||
queue.delete(id)
|
||||
}
|
||||
const now = () => clock
|
||||
|
||||
function tick(seconds) {
|
||||
const steps = Math.round(seconds * 60)
|
||||
for (let i = 0; i < steps; i++) {
|
||||
clock += STEP_MS
|
||||
for (const id of [...queue.keys()]) {
|
||||
const cb = queue.get(id)
|
||||
if (!cb) continue
|
||||
queue.delete(id)
|
||||
cb(clock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sandbox = { console, Math, Date, JSON }
|
||||
vm.createContext(sandbox)
|
||||
vm.runInContext(code, sandbox, { filename: 'scene.bundle.js' })
|
||||
const engine = sandbox.WKEngine
|
||||
|
||||
const fire = (type, event) => {
|
||||
for (const fn of [...(listeners[type] ?? [])]) fn(event)
|
||||
}
|
||||
const makeEvent = (x, y, extra = {}) => ({
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
button: 0,
|
||||
pointerId: 1,
|
||||
ctrlKey: false,
|
||||
deltaY: 0,
|
||||
deltaMode: 0,
|
||||
defaultPrevented: false,
|
||||
preventDefault() {
|
||||
this.defaultPrevented = true
|
||||
},
|
||||
stopPropagation() {},
|
||||
...extra,
|
||||
})
|
||||
|
||||
return {
|
||||
mountTower: engine.mountTower,
|
||||
engine,
|
||||
canvas,
|
||||
counts,
|
||||
seenTexts,
|
||||
rects,
|
||||
texts,
|
||||
listeners,
|
||||
listenerOptions,
|
||||
opts: (extra = {}) => ({ raf, caf, now, dpr, ...extra }),
|
||||
tick,
|
||||
jump(seconds) {
|
||||
clock += seconds * 1000
|
||||
},
|
||||
rafPending: () => queue.size,
|
||||
setCssWidth(w) {
|
||||
width = w
|
||||
},
|
||||
click(cssX, cssY) {
|
||||
fire('click', makeEvent(cssX, cssY))
|
||||
},
|
||||
pointer(type, cssX, cssY, extra = {}) {
|
||||
fire(type, makeEvent(cssX, cssY, extra))
|
||||
},
|
||||
wheel(cssX, cssY, deltaY, { ctrlKey = false } = {}) {
|
||||
const event = makeEvent(cssX, cssY, { deltaY, ctrlKey })
|
||||
fire('wheel', event)
|
||||
return event
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Aufzeichnender Canvas-Kontext für die Engine-Tests (Muster: Proxy aus entwuerfe/smoke.js).
|
||||
// fillRect/fillText werden mit dem aktuellen fillStyle/font notiert, alles Übrige ist ein No-op.
|
||||
|
||||
/**
|
||||
* makeCtx({ width, height }) → { ctx, rects, texts, strokes, counts, alphas }
|
||||
* rects: [{ x, y, w, h, fill }] jedes fillRect
|
||||
* texts: [{ text, x, y, font }] jedes fillText (Nicht-String wirft wie im Smoke-Test)
|
||||
* strokes: [{ text, x, y, style, width, rects, texts }] jedes strokeText (style/width = strokeStyle/lineWidth, rects/texts =
|
||||
* Anzahl der bis dahin gezeichneten fillRect/fillText: so ist die Reihenfolge prüfbar)
|
||||
* counts: { save, restore } Aufrufe von ctx.save()/ctx.restore()
|
||||
* alphas: [] jede Zuweisung an ctx.globalAlpha
|
||||
*/
|
||||
export function makeCtx({ width = 960, height = 768 } = {}) {
|
||||
const rects = []
|
||||
const texts = []
|
||||
const strokes = []
|
||||
const counts = { save: 0, restore: 0 }
|
||||
const alphas = []
|
||||
const target = { canvas: { width, height }, fillStyle: '#000000', font: '10px sans-serif', globalAlpha: 1 }
|
||||
const ctx = new Proxy(target, {
|
||||
get(t, prop) {
|
||||
if (typeof prop === 'symbol') return undefined
|
||||
if (prop === 'save') return () => { counts.save++ }
|
||||
if (prop === 'restore') return () => { counts.restore++ }
|
||||
if (prop === 'measureText') return (text) => ({ width: String(text).length * 7 })
|
||||
if (prop === 'fillRect') return (x, y, w, h) => { rects.push({ x, y, w, h, fill: t.fillStyle }) }
|
||||
if (prop === 'fillText') {
|
||||
return (text, x, y) => {
|
||||
if (typeof text !== 'string') throw new Error('fillText mit Nicht-String: ' + text)
|
||||
texts.push({ text, x, y, font: t.font })
|
||||
}
|
||||
}
|
||||
if (prop === 'strokeText') {
|
||||
return (text, x, y) => {
|
||||
if (typeof text !== 'string') throw new Error('strokeText mit Nicht-String: ' + text)
|
||||
strokes.push({ text, x, y, style: t.strokeStyle, width: t.lineWidth, rects: rects.length, texts: texts.length })
|
||||
}
|
||||
}
|
||||
if (prop === 'createLinearGradient' || prop === 'createRadialGradient') return () => ({ addColorStop() {} })
|
||||
if (prop === 'getImageData') return () => ({ data: new Uint8ClampedArray(4) })
|
||||
if (prop in t) return t[prop]
|
||||
return () => {}
|
||||
},
|
||||
set(t, prop, value) {
|
||||
if (prop === 'globalAlpha') alphas.push(value)
|
||||
t[prop] = value
|
||||
return true
|
||||
},
|
||||
})
|
||||
return { ctx, rects, texts, strokes, counts, alphas }
|
||||
}
|
||||
Reference in new issue
Block a user