start reworking platform axis labels

This commit is contained in:
Stewart Allen 2025-12-26 10:06:53 -05:00
commit fc6ea03f8d
2 changed files with 181 additions and 26 deletions

View file

@ -1230,7 +1230,7 @@ self.kiri.load(api => {
if (!bound) {
return;
}
if (devsel.extras?.bbl && !api.ui.deviceSave.disabled) {
if (devsel.extras?.bbl && !api.ui.devices.save.disabled) {
device = devsel;
printers = devsel.extras.bbl;
bound.bblman.classList.remove('hide');

View file

@ -444,6 +444,129 @@ function canvasInMesh(w, h, textAlign, textBaseline, color, size) {
return { context, mesh };
}
/**
* Font atlas for 3D text labels.
* Generates a texture atlas with characters 0-9, -, X, Y at startup.
*/
let fontAtlas = null;
/**
* Create bitmap font atlas for grid labels.
* Generates texture with characters: 0-9, -, X, Y
* @returns {object} Atlas data with texture and UV mapping
*/
function createFontAtlas() {
const chars = '0123456789-XY';
const canvas = DOC.createElement('canvas');
const ctx = canvas.getContext('2d');
const charSize = 64;
canvas.width = charSize * chars.length;
canvas.height = charSize;
// Clear to transparent
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Load Russo One font or fallback to sans-serif
ctx.font = `bold ${charSize * 0.8}px 'Russo One', sans-serif`;
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const uvMap = {};
for (let i = 0; i < chars.length; i++) {
ctx.fillText(chars[i], i * charSize + charSize/2, charSize/2);
uvMap[chars[i]] = {
uStart: i / chars.length,
uEnd: (i + 1) / chars.length
};
}
const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.needsUpdate = true;
console.log('Font atlas created', { width: canvas.width, height: canvas.height, chars: chars.length });
return { texture, uvMap, charSize };
}
/**
* Create 3D text label using bitmap font atlas.
* @param {string} text - Text to render
* @param {number} size - Character size
* @param {string|number} color - Text color (CSS string or hex number)
* @param {string} align - Horizontal alignment ('left', 'center', 'right')
* @returns {THREE.Group} Group containing character meshes
*/
function create3DTextLabel(text, size, color, align = 'center') {
if (!fontAtlas) {
fontAtlas = createFontAtlas();
}
const { texture, uvMap, charSize } = fontAtlas;
const group = new THREE.Group();
const charWidth = size * 0.6; // Narrower spacing for better appearance
const totalWidth = text.length * charWidth;
// Convert CSS color string to THREE.Color if needed
let threeColor = 0x333333; // default gray
if (color) {
if (typeof color === 'string') {
threeColor = new THREE.Color(color);
} else {
threeColor = color;
}
}
// Calculate starting X based on alignment
let startX = 0;
if (align === 'center') {
startX = -totalWidth / 2;
} else if (align === 'right') {
startX = -totalWidth;
}
// Create a mesh for each character
for (let i = 0; i < text.length; i++) {
const char = text[i];
const uvData = uvMap[char];
if (!uvData) {
console.warn('Character not in atlas:', char);
continue;
}
// Create plane geometry for this character
const geometry = new THREE.PlaneGeometry(charWidth, size);
const uvAttr = geometry.attributes.uv;
// Set UV coordinates to show only this character from atlas
// PlaneGeometry UVs: bottom-left, bottom-right, top-left, top-right
uvAttr.setXY(0, uvData.uStart, 0); // bottom-left
uvAttr.setXY(1, uvData.uEnd, 0); // bottom-right
uvAttr.setXY(2, uvData.uStart, 1); // top-left
uvAttr.setXY(3, uvData.uEnd, 1); // top-right
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.9,
color: threeColor,
depthWrite: false,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.x = startX + i * charWidth + charWidth / 2;
group.add(mesh);
}
return group;
}
function setRulers(
xon = ruler.xon,
@ -475,50 +598,82 @@ function updateRulers() {
oldView = ruler.view,
view = ruler.view = new THREE.Group();
if (xon && axesOn) {
let xPadding = labelSize * 4,
canvas = canvasInMesh(x + xPadding, labelSize * 3, 'center', 'top', rulerColor, labelSize),
context = canvas.context,
mesh = canvas.mesh;
if (!(context && mesh)) return;
console.log('updateRulers called', {
xon, yon, axesOn, labelSize, rulerColor, factor,
x1: ruler.x1, x2: ruler.x2, y1: ruler.y1, y2: ruler.y2,
xo: ruler.xo, yo: ruler.yo,
originX: grid.origin.x, originY: grid.origin.y
});
if (xon && axesOn) {
// Create X-axis numeric labels
// Original canvas code draws at canvas X: ruler.xo + i + xPadding/2
// Canvas is centered at world X=0, canvas width = x + xPadding
// So world X = (canvasX - canvasWidth/2) = (ruler.xo + i + xPadding/2) - (x + xPadding)/2
// = ruler.xo + i - x/2 = ruler.xo + i - w
if (platformBelt) {
for (let i = 0; i <= ruler.x2; i += grid.unitMajor) {
context.fillText((i * factor).round(1).toString(), ruler.x2 - i + xPadding / 2, 0);
const value = (i * factor).round(1).toString();
const label = create3DTextLabel(value, labelSize, rulerColor, 'center');
label.position.set(ruler.x2 - i, -h - labelSize * 2, zp);
label.rotation.x = Math.PI;
view.add(label);
}
} else {
for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) {
context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0);
const value = (i * factor).round(1).toString();
const label = create3DTextLabel(value, labelSize, rulerColor, 'center');
label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp);
label.rotation.x = Math.PI;
view.add(label);
}
for (let i = 0; i <= ruler.x2; i += grid.unitMajor) {
context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0);
const value = (i * factor).round(1).toString();
const label = create3DTextLabel(value, labelSize, rulerColor, 'center');
label.position.set(ruler.xo + i - w, -h - labelSize * 2, zp);
label.rotation.x = Math.PI;
view.add(label);
}
}
context.font = (labelSize * 0.75) + 'px sans-serif';
context.fillText(xlabel, (x + xPadding) / 2, labelSize * 1.5);
mesh.position.set(0, - h - labelSize * 2, zp);
view.add(mesh);
// Create X-axis label
const xLabel = create3DTextLabel(xlabel, labelSize * 0.75, rulerColor, 'center');
xLabel.position.set(0, -h - labelSize * 3.5, zp);
xLabel.rotation.x = Math.PI;
view.add(xLabel);
}
if (yon && axesOn) {
let yPadding = labelSize,
canvas = canvasInMesh(labelSize * 4, y + yPadding, 'end', 'middle', rulerColor, labelSize),
context = canvas.context,
mesh = canvas.mesh;
if (!(context && mesh)) return;
// Create Y-axis numeric labels
// Original canvas code draws at canvas Y: y - (ruler.yo + i) + yPadding/2
// Canvas is centered at world Y=0, canvas height = y + yPadding
// So world Y = (canvasY - canvasHeight/2) = [y - (ruler.yo + i) + yPadding/2] - (y + yPadding)/2
// = y/2 - ruler.yo - i = h - ruler.yo - i
for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) {
context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2);
const value = (i * factor).round(1).toString();
const label = create3DTextLabel(value, labelSize, rulerColor, 'right');
label.position.set(-w - labelSize * 2 - 5, h - ruler.yo - i, zp);
label.rotation.x = Math.PI;
view.add(label);
}
for (let i = 0; i <= ruler.y2; i += grid.unitMajor) {
context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2);
const value = (i * factor).round(1).toString();
const label = create3DTextLabel(value, labelSize, rulerColor, 'right');
label.position.set(-w - labelSize * 2 - 5, h - ruler.yo - i, zp);
label.rotation.x = Math.PI;
view.add(label);
}
context.font = (labelSize * 0.75) + 'px sans-serif';
context.fillText(ylabel, labelSize * 1.25, (y + yPadding) / 2);
mesh.position.set(-w - labelSize * 2 - 5, 0, zp);
view.add(mesh);
// Create Y-axis label
const yLabel = create3DTextLabel(ylabel, labelSize * 0.75, rulerColor, 'center');
yLabel.position.set(-w - labelSize * 5, 0, zp);
yLabel.rotation.x = Math.PI;
view.add(yLabel);
}
Space.scene.remove(oldView);