Skip to content

Instantly share code, notes, and snippets.

@Hermann-SW
Created October 22, 2025 19:46
Show Gist options
  • Select an option

  • Save Hermann-SW/57bb787c3a971fbaa57d96a196f1f534 to your computer and use it in GitHub Desktop.

Select an option

Save Hermann-SW/57bb787c3a971fbaa57d96a196f1f534 to your computer and use it in GitHub Desktop.
work version for 4D in 3D display of n=p*q as sum of 4 squares
// https://hermann-sw.github.io/lattice_sphere_cmp/
// https://github.com/Hermann-sw/lattice_sphere_cmp
// https://math.stackexchange.com/questions/4917740/which-faces-does-sphere-lattice-polyhedron-operatornamehullp-in-mathbbz
//
const jscad = require('@jscad/modeling')
const { star } = jscad.primitives
const { hull } = jscad.hulls
const { geom3, poly3 } = jscad.geometries
const { colorize } = jscad.colors
const { cuboid, sphere, cylinder, circle } = jscad.primitives
const { rotate, translate, translateX, scale:scale3d } = jscad.transforms
const { angle, add, length, subtract, scale, dot } = jscad.maths.vec3
const { vec3, plane } = jscad.maths
const { fromPoints } = jscad.maths.plane
const { vectorText } = jscad.text
const { union } = jscad.booleans
const { extrudeLinear } = jscad.extrusions
const { measureBoundingBox } = jscad.measurements
const { hullChain } = jscad.hulls
const det = ([[a,b,c],[d,e,f],[g,h,i]]) => a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g)
const rep = (A,b,i) => (C=[A[0].slice(),A[1].slice(),A[2].slice()],
C[0][i]=b[0],C[1][i]=b[1],C[2][i]=b[2], C)
const solve = (A,b) =>
(d=det(A), [det(rep(A,b,0))/d, det(rep(A,b,1))/d, det(rep(A,b,2))/d])
const gcd = (a,b) => b ? gcd(b, a % b) : Math.abs(a)
const frac = (i,d) => (g=gcd(i,d),(d==g)?i/g:[i/g,d/g])
const solver = (A,b) => (d=det(A),
[frac(det(rep(A,b,0)),d), frac(det(rep(A,b,1)),d), frac(det(rep(A,b,2)),d)])
function txt(mesg, w) {
const lineRadius = w / 8 // 2
const lineCorner = circle({ radius: lineRadius })
const lineSegmentPointArrays = vectorText({ x: 0, y: 0, height: 0.125, input: mesg })
const lineSegments = []
lineSegmentPointArrays.forEach(function(segmentPoints) {
const corners = segmentPoints.map((point) => translate(point, lineCorner))
lineSegments.push(hullChain(corners))
})
const message2D = union(lineSegments)
ret = extrudeLinear({ height: w }, message2D)
return ret
}
let map = new Map()
function txt2(mesg, w) {
let xofs = 0
let ret = []
for(i in mesg){
if(map.get(mesg[i])===undefined) {
map.set(mesg[i], txt(mesg[i], w))
}
ret.push(translateX(xofs, map.get(mesg[i])))
xofs += measureBoundingBox(map.get(mesg[i]))[1][0]
}
return ret
}
function vtxt(p1, num) {
str = num.toString()
la1 = Math.atan2(p1[1], p1[0])
ph1 = Math.PI/2-Math.acos(p1[2]/vec3.length(p1))
lab = txt2(str, 0.05)
return translate([0, 0, 0.2],
rotate([0, 0, la1],
rotate([0, -ph1, 0],
translate([vec3.length(p1)+0.1, -measureBoundingBox(lab[lab.length-1])[1][0]/2],
rotate([Math.PI/2, 0, Math.PI/2],
colorize([1, 1, 1],
lab
)
)
)
)
)
)
}
segments = 10
const oneCylinder = cylinder({radius: 1, height: 1, segments: segments})
let reusedsphere = undefined
function fastvertex(c) { return(translate(c, reusedsphere)) }
function edge(_v, _w, r = 0.05, segs = segments) {
d = [0, 0, 0]
w = [0, 0, 0]
subtract(d, _w, _v)
add(w, _v, _w)
scale(w, w, 0.5)
return translate(w,
rotate([0, Math.acos(d[2]/length(d)), Math.atan2(d[1], d[0])],
scale3d([r,r,length(d)],oneCylinder)
)
)
}
let nsqrt = 0
const isPointOnPlane = (pla, pt, tol=1e-10) =>
Math.abs(vec3.dot(pt, pla) - pla[3]) < tol
const arePointsOnPlane = (pla, pts, tol=1e-10) =>
pts.every((p) => isPointOnPlane(pla, p, tol))
// issue #1347 workaround: works for all points in same plane as well
function fromPointsConvex(pts) {
pla = plane.fromPoints(plane.create(), ...pts)
if (!arePointsOnPlane(pla, pts)) {
return geom3.fromPointsConvex(pts)
}
np = vec3.subtract(vec3.create(),
pts[0],
vec3.scale(vec3.create(), pla, 1e-3)
)
g = geom3.fromPointsConvex([np, ...pts])
p3 = g.polygons.reduce((a,p) =>
(p!==undefined&&!p.vertices.some((v)=>vec3.equals(v,np)))?a=p:a,{})
g.polygons = [p3, poly3.invert(p3)]
return g
}
const assert = (b) => {if(!b){throw("assert")}}
var x=0
var sc = Math.sqrt(65-x*x)
var er = 0.04
function cart2pol(p, f=sc) {
return [Math.atan2(p[1],p[0]), Math.acos(p[2]/f)]
}
function edge2(_p1, _p2) {
const v = _p1 // map3D(coords[_p1][0], coords[_p1][1])
const w = _p2 // map3D(coords[_p2][0], coords[_p2][1])
const p1 = cart2pol(v)
const p2 = cart2pol(w)
// al/la/ph: alpha/lambda/phi | lxy/sxy: delta lambda_xy/sigma_xy
// https://en.wikipedia.org/wiki/Great-circle_navigation#Course
const la1 = p1[0]
const la2 = p2[0]
const l12 = la2 - la1
const ph1 = Math.PI/2 - p1[1]
const ph2 = Math.PI/2 - p2[1]
const al1 = Math.atan2(Math.cos(ph2)*Math.sin(l12), Math.cos(ph1)*Math.sin(ph2)-Math.sin(ph1)*Math.cos(ph2)*Math.cos(l12))
// delta sigma_12
// https://en.wikipedia.org/wiki/Great-circle_distance#Formulae
const s12 = Math.acos(Math.sin(ph1)*Math.sin(ph2)+Math.cos(ph1)*Math.cos(ph2)*Math.cos(l12))
return rotate([0, -ph1, la1],
rotate([Math.PI/2-al1, 0, 0],
makeArc2(sc, s12, 64)
)
)
}
// cool "makeArc2()" replaces "extrudeRotate()", from [email protected]
//
let cachedCylinder = rotate([-Math.PI/2,0,0],cylinder({radius:er, height:1, center:[0,0,0.5]}))
function makeArc2(radius, angle, segments=64) {
let correction = 0
if(angle < 0){
correction = angle
angle *= -1
}
// match how jscad calculates segments
let stepA = Math.PI/(segments)*2
let steps = Math.ceil(angle / stepA)
stepA = angle / steps
let offset = 0, next=0
let out = []
while(offset < angle){
next += stepA
if(next > angle) next=angle
let len = (next-offset) * radius
let x = Math.cos(offset) * radius
let y = Math.sin(offset) * radius
let part = colorize([0,0,1],translate([x,y,0],rotate([0,0,(next-(next-offset)/2)],
jscad.transforms.scale([1,len,1], cachedCylinder)
)))
out.push( correction ? rotate([0,0,correction], part):part)
offset = next
}
if(!out.length) { throw("makeArc2(",radius,",",angle,",",segments,") problem") }
return out
}
function main(params) {
n = params.p*params.q
nsqrt = Math.sqrt(n)
reusedsphere = sphere({radius:0.014*nsqrt, segments: segments})
m = Math.floor(Math.sqrt(n))
out = []
edges = []
vertices = []
outside = []
for(k=0;k<=m;++k)
{
for(x=-m;x<=m;++x)
for(y=-m;y<=m;++y)
for(z=-m;z<=m;++z)
if (x*x+y*y+z*z+k*k==n)
out.push([x,y,z,k])
h = geom3.fromPointsConvex(out)
out=[]
sc=Math.sqrt(n-k*k)
h.polygons.forEach((p)=>{
vs = p.vertices
for(i=0;i<vs.length;i++){
edges.push(edge2(vs[i], vs[(i+1)%vs.length]))
vertices.push(fastvertex(vs[i]))
}
if(params.text===true)
vs.forEach((v)=>outside.push(vtxt(v,JSON.stringify(v))))
})
}
return [edges, vertices, outside]
}
function getParameterDefinitions() {
return [
{ name: 'p', type: 'choice', values: [5, 13, 17, 29, 37, 41, 53, 61, 73, 89, 97], initial: 5, caption: 'p' },
{ name: 'q', type: 'choice', values: [5, 13, 17, 29, 37, 41, 53, 61, 73, 89, 97], initial: 13, caption: 'q' },
{ name: 'text', type: 'checkbox', checked: false, caption: 'text:' },
]
}
module.exports = { main, getParameterDefinitions }
@Hermann-SW
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment