diff --git a/VERSION b/VERSION
index 2c8f5ac..f5d2f80 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.3.71-beta
+0.3.72-beta
diff --git a/public/css/teach.css b/public/css/teach.css
index efe0c61..049e4c5 100644
--- a/public/css/teach.css
+++ b/public/css/teach.css
@@ -342,7 +342,10 @@
.mm-pop .mp-ls[data-ls=dot] i{border-top-style:dotted;}
.mm-pop .mp-x{margin-left:auto; font-weight:900; color:var(--red); font-size:12px;}
/* physical tools: ruler, set square, magnifier */
- .dtool{position:absolute; z-index:3; touch-action:none; user-select:none; cursor:grab; left:120px; top:120px;}
+ /* boven de inktlaag (4000): net als een echte lat ligt het gereedschap óp
+ het bord - ook in penmodus te verplaatsen en in te stellen; tekenen
+ begint ernaast en kleeft aan de tekenrand */
+ .dtool{position:absolute; z-index:4200; touch-action:none; user-select:none; cursor:grab; left:120px; top:120px;}
.dtool:active{cursor:grabbing;}
.dtool .dtool-x{
position:absolute; top:-10px; right:-10px; width:24px; height:24px; border-radius:50%;
@@ -356,6 +359,11 @@
}
.dtool-ruler{width:600px; height:76px;}
.dtool-geo{width:340px; height:184px;}
+ /* alleen de driehoek zelf is aan te raken - de lege hoeken van de
+ rechthoekige box laten aanrakingen door naar het bord, anders kon er
+ niet vlak naast de schuine randen getekend worden */
+ .dtool-geo{pointer-events:none;}
+ .dtool-geo svg polygon,.dtool-geo .dtool-opts,.dtool-geo .dtool-x,.dtool-geo .dtool-rot{pointer-events:auto;}
.dtool-opts{position:absolute; top:-12px; left:8px; display:flex; gap:5px;}
.dtool-opts button{
width:24px; height:24px; border-radius:50%; border:none; background:var(--surface);
diff --git a/public/js/board.js b/public/js/board.js
index 0b057d0..d1be138 100644
--- a/public/js/board.js
+++ b/public/js/board.js
@@ -578,6 +578,7 @@ function makeMovable(t){
const near = Math.round(a/45)*45;
if(Math.abs(a-near) < 4) a = near;
ang = a;
+ t._ang = ang; /* voor het kleven van de pen aan de gedraaide rand */
t.style.transform = `rotate(${ang}deg)`;
const shown = Math.round(((ang % 360) + 360) % 360);
badge.textContent = shown + "°";
@@ -636,6 +637,8 @@ function toggleDTool(kind){
${cmLen} cm
${ticks}`;
+ /* tekenranden voor het kleven van de pen (bord-lokale coördinaten) */
+ t._edges = [[[0,0],[L,0]], [[0,76],[L,76]]];
const lenBtn = t.querySelector(".do-len");
lenBtn.textContent = cmLen + " cm";
lenBtn.title = T("dtLen");
@@ -654,37 +657,76 @@ function toggleDTool(kind){
t.style.height = "76px";
drawRuler();
}else if(kind==="geo"){
- t._sz = 340; t._hc = false;
+ /* geodriehoek zoals de echte: cm-schaal op de hypotenusa met 0 in het
+ midden, gradenbogen in twee richtingen, en twee niveaus:
+ basis - grote cijfers, alleen de hoofdhoeken (30/45/60/90/…) en
+ hele centimeters, voor jonge leerlingen;
+ volledig - mm-streepjes, graden per 2° met dubbele schaal (links- én
+ rechtsom, zoals op een echte geodriehoek) en evenwijdige
+ hulplijnen per cm om parallelle lijnen te tekenen. */
+ const GEO_CMS = [6, 8, 12];
+ t._cm = 8; t._hc = false; t._full = false;
const drawGeo = ()=>{
- const W = t._sz, H = W/2 + 22, hc = t._hc;
+ const maxCm = Math.max(6, Math.floor((board.clientWidth-40)/50));
+ const cmLen = Math.min(t._cm, maxCm);
+ const W = cmLen*50 + 28, H = W/2 + 22, hc = t._hc, full = t._full;
const ink = hc ? "#000" : "#1e4f74";
- const bg = hc ? "rgba(255,214,0,.9)" : "rgba(185,222,248,.6)";
+ const bg = hc ? "rgba(255,214,0,.9)" : "rgba(185,222,248,.55)";
const halo = `stroke="${hc?"#ffd400":"#ffffff"}" stroke-width="3.5" paint-order="stroke" stroke-linejoin="round"`;
- const ax = W/2, ay = 16, by = H-18; /* apex + baseline */
+ const ax = W/2, ay = 16, by = H-18; /* apex + tekenrand */
const half = W/2 - 14;
- let cmTicks = "";
- for(let i=25; i<=half-16; i+=25){
- const cm = i%50===0;
+ /* cm-schaal op de tekenrand, 0 in het midden; volledig ook mm */
+ let cmTicks = `0`;
+ const stepPx = full ? 5 : 25;
+ for(let i=stepPx; i<=half; i+=stepPx){
+ const cm = i%50===0, halfCm = i%25===0;
+ const len = cm ? 13 : halfCm ? 9 : 5;
[ax-i, ax+i].forEach(x=>{
- cmTicks += ``;
+ cmTicks += ``;
});
- if(cm){
+ if(cm && i<=half-14){
[ax-i, ax+i].forEach(x=>{
cmTicks += `${i/50}`;
});
}
}
- /* degree arc with labels INSIDE the arc so they stay readable */
+ /* gradenboog: basis = hoofdhoeken groot; volledig = per 2° met dubbele
+ schaal (buiten linksom, binnen rechtsom) zoals op een echte geodriehoek */
let degs = "";
- const R = half*0.60;
- for(let d=10; d<180; d+=10){
+ const R = half*0.62;
+ const degStep = full ? 2 : 10;
+ for(let d=degStep; d<180; d+=degStep){
+ const major = full ? d%10===0 : d%30===0 || d===45 || d===135;
const rad = d*Math.PI/180;
- const x1 = ax - Math.cos(rad)*(R-7), y1 = by - Math.sin(rad)*(R-7);
+ const x1 = ax - Math.cos(rad)*(R-(major?10:5)), y1 = by - Math.sin(rad)*(R-(major?10:5));
const x2 = ax - Math.cos(rad)*R, y2 = by - Math.sin(rad)*R;
- degs += ``;
- if(d%30===0 && d>=30 && d<=150){
- const xt = ax - Math.cos(rad)*(R-22), yt = by - Math.sin(rad)*(R-22);
- degs += `${d}°`;
+ degs += ``;
+ }
+ /* 10° en 170° liggen vrijwel op de baseline en zouden de cm-cijfers
+ overlappen - net als op een echte geodriehoek beginnen de labels
+ daar pas een stap hoger (de streepjes staan er wel) */
+ const labels = full
+ ? Array.from({length:15}, (_,i)=>(i+2)*10)
+ : [30,45,60,90,120,135,150];
+ labels.forEach(d=>{
+ const rad = d*Math.PI/180;
+ if(full){
+ const xo = ax - Math.cos(rad)*(R-20), yo = by - Math.sin(rad)*(R-20);
+ const xi = ax - Math.cos(rad)*(R-36), yi = by - Math.sin(rad)*(R-36);
+ degs += `${d}`;
+ degs += `${180-d}`;
+ }else{
+ const xt = ax - Math.cos(rad)*(R-24), yt = by - Math.sin(rad)*(R-24);
+ degs += `${d}°`;
+ }
+ });
+ /* volledig: evenwijdige hulplijnen per cm, parallel aan de tekenrand */
+ let parallels = "";
+ if(full){
+ for(let yy=by-50; yy>ay+24; yy-=50){
+ const hw = half * (yy-ay)/(by-ay) - 10;
+ if(hw < 30) break;
+ parallels += ``;
}
}
t.style.width = W+"px"; t.style.height = H+"px";
@@ -692,19 +734,27 @@ function toggleDTool(kind){
- 0
- ${cmTicks}${degs}`;
+ 90°
+ ${parallels}${cmTicks}${degs}`;
+ /* tekenranden voor het kleven van de pen (bord-lokale coördinaten) */
+ t._edges = [[[14,by],[W-14,by]], [[14,by],[ax,ay]], [[W-14,by],[ax,ay]]];
+ const lenBtn = t.querySelector(".do-len");
+ lenBtn.textContent = cmLen + " cm";
+ lenBtn.title = T("dtLen");
+ const modeBtn = t.querySelector(".do-mode");
+ modeBtn.textContent = T(full ? "dtModeFull" : "dtModeBasic");
+ modeBtn.title = T("dtMode");
};
t.innerHTML = `
-
+
+
`;
- const geoLen = t.querySelector(".do-len");
- geoLen.title = T("dtLen");
- geoLen.addEventListener("click", ()=>{
- t._sz = t._sz===340 ? 440 : t._sz===440 ? 280 : 340; drawGeo();
+ t.querySelector(".do-len").addEventListener("click", ()=>{
+ t._cm = GEO_CMS[(GEO_CMS.indexOf(t._cm)+1) % GEO_CMS.length]; drawGeo();
});
+ t.querySelector(".do-mode").addEventListener("click", ()=>{ t._full = !t._full; drawGeo(); });
const geoHc = t.querySelector(".do-hc");
geoHc.title = T("dtContrast");
geoHc.addEventListener("click", ()=>{ t._hc = !t._hc; drawGeo(); });
@@ -841,6 +891,50 @@ const bPos = ev=>{
/* ratio-based: correct under any display zoom */
return [(ev.clientX-r.left)*(bInk.width/r.width), (ev.clientY-r.top)*(bInk.height/r.height)];
};
+/* ---- langs de liniaal/geodriehoek tekenen -----------------------------------
+ Elke rand van de liniaal en geodriehoek is een "tekenrand". Begint een haal
+ binnen 16px van zo'n rand, dan wordt die rand voor de hele haal vergrendeld
+ en wordt elk punt erop geprojecteerd: een kaarsrechte lijn, precies zoals
+ tekenen tegen een echte lat. Verder dan ~60px van de rand laat de pen weer
+ los (bewust weggetrokken). Randen draaien mee met het gereedschap. */
+let bSnap = null;
+function toolEdges(){
+ const edges = [];
+ ["ruler","geo"].forEach(kind=>{
+ const t = DTOOLS[kind];
+ if(!t || !t._edges) return;
+ const rad = (t._ang||0)*Math.PI/180;
+ const cosA = Math.cos(rad), sinA = Math.sin(rad);
+ const cx = t.offsetLeft + t.offsetWidth/2, cy = t.offsetTop + t.offsetHeight/2;
+ const rot = ([x,y])=>{
+ const dx = x - t.offsetWidth/2, dy = y - t.offsetHeight/2;
+ return [cx + dx*cosA - dy*sinA, cy + dx*sinA + dy*cosA];
+ };
+ t._edges.forEach(seg=>edges.push([rot(seg[0]), rot(seg[1])]));
+ });
+ return edges;
+}
+function nearestEdge(px, py){
+ let best = null;
+ toolEdges().forEach(([a,b])=>{
+ const ux = b[0]-a[0], uy = b[1]-a[1], len = Math.hypot(ux,uy);
+ if(len < 1) return;
+ const along = Math.max(0, Math.min(len, ((px-a[0])*ux + (py-a[1])*uy)/len));
+ const qx = a[0] + ux/len*along, qy = a[1] + uy/len*along;
+ const d = Math.hypot(px-qx, py-qy);
+ if(!best || d < best.d) best = { d, ax:a[0], ay:a[1], ux:ux/len, uy:uy/len, len };
+ });
+ return best;
+}
+function snapPoint(x, y){
+ if(!bSnap) return [x, y];
+ const k = bInk.width / board.clientWidth; /* canvas-px per bord-px */
+ const px = x/k, py = y/k;
+ const along = Math.max(0, Math.min(bSnap.len, ((px-bSnap.ax)*bSnap.ux + (py-bSnap.ay)*bSnap.uy)));
+ const qx = bSnap.ax + bSnap.ux*along, qy = bSnap.ay + bSnap.uy*along;
+ if(Math.hypot(px-qx, py-qy) > 60){ bSnap = null; return [x, y]; }
+ return [qx*k, qy*k];
+}
bInk.addEventListener("pointerdown", e=>{
if(e.pointerType==="mouse" && e.button!==0) return;
e.preventDefault();
@@ -849,13 +943,22 @@ bInk.addEventListener("pointerdown", e=>{
bDraw = true;
PEN.nib = PEN.size*0.8;
[bx,by] = bPos(e);
+ bSnap = null;
+ if(PEN.tool!=="erase" && PEN.tool!=="hand"){
+ const k = bInk.width / board.clientWidth;
+ const hit = nearestEdge(bx/k, by/k);
+ if(hit && hit.d <= 16) bSnap = hit;
+ }
+ [bx,by] = snapPoint(bx, by);
bMidX = bMidY = null;
bStroke(bx+0.01, by+0.01, e.pressure, e.pointerType);
});
bInk.addEventListener("pointermove", e=>{
if(!bDraw) return;
(e.getCoalescedEvents ? e.getCoalescedEvents() : [e]).forEach(ev=>{
- const [x,y] = bPos(ev); bStroke(x, y, ev.pressure, ev.pointerType);
+ const [rx,ry] = bPos(ev);
+ const [x,y] = snapPoint(rx, ry);
+ bStroke(x, y, ev.pressure, ev.pointerType);
});
});
const bStop = ()=>{
@@ -874,6 +977,7 @@ const bStop = ()=>{
markBoardChange();
}
bDraw = false;
+ bSnap = null;
bCtx.globalAlpha = 1;
if(DTOOLS.lens) DTOOLS.lens.lensDraw();
};
diff --git a/public/js/core.js b/public/js/core.js
index deeb28d..632db56 100644
--- a/public/js/core.js
+++ b/public/js/core.js
@@ -2,7 +2,7 @@
"use strict";
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */
-const VERSION = "0.3.71-beta";
+const VERSION = "0.3.72-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@@ -244,6 +244,8 @@ const I18N = {
parentUnlinkWarn:"Verwijdert alleen de koppeling; het account van je kind blijft bestaan.",
parentProgress:"📈 Voortgang", parentProgressEmpty:"Nog geen voortgang.",
dtLen:"Lengte", dtContrast:"Hoog contrast",
+ dtMode:"Basis: grote hoofdhoeken en hele centimeters. Volledig: graden per 2° met dubbele schaal, millimeters en evenwijdige hulplijnen.",
+ dtModeBasic:"basis", dtModeFull:"volledig",
pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.",
pupilWatching:"meekijken",
amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Systeem",
@@ -515,6 +517,8 @@ const I18N = {
parentUnlinkWarn:"Removes only the link; your child's account remains.",
parentProgress:"📈 Progress", parentProgressEmpty:"No progress yet.",
dtLen:"Length", dtContrast:"High contrast",
+ dtMode:"Basic: big main angles and whole centimetres. Full: degrees every 2° with a double scale, millimetres and parallel guide lines.",
+ dtModeBasic:"basic", dtModeFull:"full",
pupilEmpty:"Your teacher hasn't set up anything for you yet.",
pupilWatching:"watching",
amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"System",