aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/props.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/props.js')
-rw-r--r--public/bluebey-studio/src/props.js63
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);