diff options
Diffstat (limited to 'public/bluebey-studio/src/props.js')
| -rw-r--r-- | public/bluebey-studio/src/props.js | 63 |
1 files changed, 62 insertions, 1 deletions
diff --git a/public/bluebey-studio/src/props.js b/public/bluebey-studio/src/props.js index a5fcbcc..ba682e4 100644 --- a/public/bluebey-studio/src/props.js +++ b/public/bluebey-studio/src/props.js @@ -344,7 +344,7 @@ export const PROP_DEFAULTS = { // `faceScale` only means something for a 看板: it grows the writing area (the // face and its frame) together, so the post and base stay put. The panel owns // that control. - sign: { x: 2.9, y: 0, z: 0.4, rotY: -0.25, scale: 1, faceScale: 1 }, + sign: { x: 2.9, y: 0, z: 0.4, rotY: -0.25, scale: 1, faceScale: 1, text: '' }, plant: { x: -2.0, y: 0, z: -1.8, rotY: 0.25, scale: 1 }, box: { x: 2.0, y: 0, z: -1.7, rotY: 0.55, scale: 1 }, can: { x: 1.7, y: 0, z: 1.5, rotY: -0.5, scale: 1 }, @@ -457,6 +457,67 @@ export function applyPropFaceScale(group, faceScale) { target.scale.setScalar(scale); } +/** + * Paint text onto a prop's writing surface (the 看板's face), or clear it. + * + * The face's material gets a `CanvasTexture`, drawn here with a system font stack + * (no font file is needed, and Japanese falls back to whatever the device has). + * Long lines and long blocks are shrunk to fit the board, and explicit newlines + * are honoured. The material is per-prop (see `materialCache`), so the map never + * leaks to another sign. + */ +export function applyPropText(group, text) { + if (typeof document === 'undefined') return; // Node (tests): no canvas + const face = propFace(group); + const material = face?.material; + if (!material) return; + const value = typeof text === 'string' ? text : ''; + + if (!value.trim()) { + if (material.map) { + material.map.dispose(); + material.map = null; + material.needsUpdate = true; + } + material.userData.signText = ''; + return; + } + if (material.userData.signText === value) return; // nothing changed + + const canvas = document.createElement('canvas'); + canvas.width = 512; + canvas.height = 330; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#f4efe2'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#241a30'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + const family = '"Hiragino Kaku Gothic ProN", "Yu Gothic", "Noto Sans JP", sans-serif'; + const lines = value.split(/\r\n|\r|\n/); + const maxWidth = canvas.width * 0.86; + let size = 150; + const fits = () => { + ctx.font = `700 ${size}px ${family}`; + return lines.every((line) => ctx.measureText(line).width <= maxWidth) + && lines.length * size * 1.15 <= canvas.height * 0.86; + }; + while (size > 18 && !fits()) size -= 2; + ctx.font = `700 ${size}px ${family}`; + const step = size * 1.15; + const startY = canvas.height / 2 - ((lines.length - 1) * step) / 2; + lines.forEach((line, index) => ctx.fillText(line, canvas.width / 2, startY + index * step)); + + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + texture.anisotropy = 4; + if (material.map) material.map.dispose(); + material.map = texture; + material.userData.signText = value; + material.needsUpdate = true; +} + /** The world-space box the prop occupies; the app drops its contact shadow from it. */ export function propBounds(group) { group.updateMatrixWorld(true); |
