Workers P2: Sichtprüfung im Browser – Schilder über den Figuren, Raumtitel über den Möbeln, Zoomknöpfe unten, Abstand der Meldung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 04:55:51 +00:00
1 parent d67f8cc471
commit 820ccd510d
9 files changed
+122 -21

No files matched your search

+11 -2
View File
@@ -2,15 +2,18 @@
// fillRect/fillText werden mit dem aktuellen fillStyle/font notiert, alles Übrige ist ein No-op.
/**
* makeCtx({ width, height }) → { ctx, rects, texts, counts, alphas }
* 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 }
@@ -27,6 +30,12 @@ export function makeCtx({ width = 960, height = 768 } = {}) {
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]
@@ -38,5 +47,5 @@ export function makeCtx({ width = 960, height = 768 } = {}) {
return true
},
})
return { ctx, rects, texts, counts, alphas }
return { ctx, rects, texts, strokes, counts, alphas }
}
+6
View File
@@ -12,3 +12,9 @@ test('jede im JSX verwendete wk-Klasse ist definiert', () => {
const css = readFileSync(new URL('../../src/styles/workers.css', import.meta.url), 'utf8'); const dir = new URL('../../src/components/workers/', import.meta.url)
for (const f of readdirSync(dir).filter((x) => x.endsWith('.jsx'))) for (const m of readFileSync(new URL(f, dir), 'utf8').matchAll(/(?<![\w-])wk-[a-z0-9-]+/g)) assert.ok(css.includes('.' + m[0]), `${f}: ${m[0]}`) // Lookbehind: das Attribut data-wk-worker ist keine Klasse
})
test('workers.css: Zoomknöpfe unten rechts (oben läge die Reihe über dem Schild der Kommandobrücke), Meldungen halten Abstand zu den Kennzahlen', () => {
const css = readFileSync(new URL('../../src/styles/workers.css', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
const rule = (selector) => css.match(new RegExp(`${selector.replace(/[.~]/g, '\\$&')}\\s*\\{([^}]*)\\}`))?.[1] ?? ''
assert.match(rule('.wk-zoom'), /bottom:\s*8px/); assert.match(rule('.wk-zoom'), /right:\s*8px/); assert.doesNotMatch(rule('.wk-zoom'), /\btop:/)
assert.match(rule('.wk-toolbar ~ .msg'), /margin-bottom:\s*16px/)
})
+37
View File
@@ -297,3 +297,40 @@ test('grauer Raum: Teamfarbton bleibt, das Schild ersetzt den Titel (kein Platz
assert.equal(grey.texts.filter((x) => x.text === SIGN).length, 1)
assert.equal(normal.texts.filter((x) => x.text === SIGN).length, 0)
})
test('Schilder: drawRooms mit signs: false lässt sie aus (Grautönung bleibt), drawSigns zeichnet genau diese Schilder an dieselbe Stelle', () => {
const look = looks({ greyRooms: new Set(['kleinanzeigen', 'portale']) })
const withSigns = draw(look)
const m = makeCtx()
R.drawRooms(m.ctx, layout, 1, look, { signs: false })
assert.equal(m.texts.filter((x) => x.text === SIGN).length, 0)
assert.equal(m.rects.length < withSigns.rects.length, true)
assert.ok(m.alphas.includes(0.45), 'die Grautönung gehört weiter zum Raum')
assert.deepEqual(m.texts.filter((x) => x.text !== SIGN).map((x) => x.text), withSigns.texts.filter((x) => x.text !== SIGN).map((x) => x.text))
const s = makeCtx()
R.drawSigns(s.ctx, layout, look)
assert.deepEqual(s.texts.map((x) => x.text), [SIGN, SIGN])
assert.deepEqual(s.texts.map((x) => [x.x, x.y]), withSigns.texts.filter((x) => x.text === SIGN).map((x) => [x.x, x.y]))
const none = makeCtx()
R.drawSigns(none.ctx, layout, R.createLook())
assert.equal(none.rects.length + none.texts.length, 0)
const noWorker = makeCtx()
R.drawSigns(noWorker.ctx, layout, looks({ greyRooms: new Set(['pult', 'tresor']) })) // Räume ohne Worker werden nie grau
assert.equal(noWorker.texts.length, 0)
})
test('Raumtitel: über den Möbeln gezeichnet (ein Monitor deckt sie nicht zu) und mit hellem Rand', () => {
const floor = layout.floors.find((f) => f.id === 'gespraeche')
const room = floor.rooms.find((r) => r.id === 'mail')
const m = makeCtx()
R.drawRooms(m.ctx, { ...layout, floors: [{ ...floor, rooms: [room] }] }, 1, R.createLook())
assert.equal(m.strokes.length, 1)
assert.equal(m.strokes[0].text, room.title)
assert.ok(m.strokes[0].style.startsWith('rgba(255, 255, 255'))
assert.equal(m.strokes[0].rects, m.rects.length, 'nach dem Titel kommt keine Fläche mehr: Möbel, Tisch und Monitor liegen darunter')
assert.ok(m.rects.length > 20)
assert.equal(m.texts.filter((x) => x.text === room.title).length, 1)
const grey = makeCtx()
R.drawRooms(grey.ctx, { ...layout, floors: [{ ...floor, rooms: [{ ...room, id: 'kleinanzeigen' }] }] }, 1, looks({ greyRooms: new Set(['kleinanzeigen']) }))
assert.equal(grey.strokes.length, 0, 'im grauen Raum steht kein Titel')
})
+5 -3
View File
@@ -212,7 +212,7 @@ test('resize() zieht den Startausschnitt nach, bis der Nutzer die Kamera bewegt
// ---------- Zeichnen ----------
test('Zeichenreihenfolge: Weiß, Außenwelt, Turm, Räume, Aufzug hinten, Figuren, Aufzug vorn, Pausenschleier', async () => {
test('Zeichenreihenfolge: Weiß, Außenwelt, Turm, Räume, Aufzug hinten, Figuren, Schilder, Aufzug vorn, Pausenschleier', async () => {
const { sb, h } = await open({ record: true })
const state = makeState({ paused: true, connected: { kim: false } })
h.setState(state)
@@ -233,12 +233,14 @@ test('Zeichenreihenfolge: Weiß, Außenwelt, Turm, Räume, Aufzug hinten, Figure
}
const outside = rectN((r) => r.fill === '#f7f4f0', 'Erde der Außenwelt')
const tower = rectN((r) => r.fill === '#e4e8ef', 'Aufzugschacht')
const rooms = textN('noch nicht angeschlossen')
const rooms = textN('Netz-Radar') // Raumtitel
const liftBack = rectN((r) => r.fill === '#f1f3f8', 'Rückwand der Kabine')
const figure = rectN((r) => r.alpha === 0.55, 'graue Figur')
const sign = textN('noch nicht angeschlossen') // das Schild liegt über dem Hut der schlafenden Figur (sonst deckt der Hut den Text zu)
const liftFront = rectN((r) => r.fill === '#ffe9a8', 'Lampe der Kabine')
const veil = textN('Pausiert')
assert.deepEqual([outside, tower, rooms, liftBack, figure, liftFront, veil].toSorted((a, b) => a - b), [outside, tower, rooms, liftBack, figure, liftFront, veil])
assert.deepEqual([outside, tower, rooms, liftBack, figure, sign, liftFront, veil].toSorted((a, b) => a - b), [outside, tower, rooms, liftBack, figure, sign, liftFront, veil])
assert.equal(sb.texts.filter((t) => t.text === 'noch nicht angeschlossen').length, 1, 'genau ein Schild, nicht zweimal')
assert.equal(sb.texts.at(-1).text, 'Pausiert')
h.destroy()
})
+17
View File
@@ -63,6 +63,23 @@ test('label: Zahl wird zu Text, Hintergrund folgt der Ausrichtung', () => {
assert.deepEqual([c.rects[0].x, c.rects[0].y, c.rects[0].w, c.rects[0].h, c.rects[0].fill], [12, 13, 36, 17, '#ffffff'])
assert.equal(c.texts[0].x, 30)
})
test('label: halo zeichnet erst einen hellen Rand (strokeText), dann den Text darüber; ohne halo kein Rand', () => {
const h = makeCtx()
S.setGrid(h.ctx, { k: 3 })
S.label(h.ctx, 'Mail-Stube', 2, 1, { size: 10, color: '#445566', halo: '#ffffff' })
assert.equal(h.strokes.length, 1)
assert.deepEqual([h.strokes[0].text, h.strokes[0].style], ['Mail-Stube', '#ffffff'])
assert.deepEqual([h.strokes[0].x, h.strokes[0].y], [h.texts[0].x, h.texts[0].y], 'Rand und Text an derselben Stelle')
assert.equal(h.strokes[0].texts, 0, 'der Rand kommt vor dem Text')
assert.ok(h.strokes[0].width >= 2, 'mindestens ein Geräte-Pixel Rand rundum')
const big = makeCtx()
S.setGrid(big.ctx, { k: 6 })
S.label(big.ctx, 'Mail-Stube', 2, 1, { size: 10, halo: '#ffffff' })
assert.ok(big.strokes[0].width > h.strokes[0].width, 'der Rand wächst mit dem Raster')
const plain = makeCtx()
S.label(plain.ctx, 'Mail-Stube', 2, 1, { size: 10 })
assert.equal(plain.strokes.length, 0)
})
test('drawWorker: Schlafen zeigt ein z über dem Hut, das mit t steigt; stehen nicht', () => {
const zs = (pose, t) => {
const { ctx, rects } = makeCtx()