2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung (konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text- Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback, falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform) fuer fluessige Interaktion ohne React-Re-Render je Frame. Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM (Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext- Import, Tauri-Compute-Boundary-PoC).
This commit is contained in:
@@ -44,21 +44,34 @@ function polyNextFromFields(last: Vec2, locks: Record<string, number>, cursor: V
|
||||
|
||||
interface PolyIdle extends CommandState {
|
||||
phase: "start";
|
||||
/** Gewählter Schließ-Modus (bleibt über den Befehl hinweg erhalten). */
|
||||
closed: boolean;
|
||||
}
|
||||
interface PolyDrawing extends CommandState {
|
||||
phase: "next";
|
||||
points: Vec2[];
|
||||
cursor: Vec2 | null;
|
||||
/** Soll der committete Zug als Ring (geschlossen) gelten? Default: offen. */
|
||||
closed: boolean;
|
||||
}
|
||||
type PolyState = PolyIdle | PolyDrawing;
|
||||
|
||||
/** Sofort-schließen-Aktion (nur ≥3 Punkte): committet als Ring und beendet. */
|
||||
const CLOSE: CmdOption = { id: "close", labelKey: "cmd.polyline.close" };
|
||||
const UNDO: CmdOption = { id: "undo", labelKey: "cmd.polyline.undo" };
|
||||
/** Toggle-Option: Ergebnis geschlossen (Ring) oder offen — im Feld-Row sichtbar. */
|
||||
const closedOption = (closed: boolean): CmdOption => ({
|
||||
id: "mode",
|
||||
labelKey: "cmd.polyline.closedOpt",
|
||||
value: closed ? "on" : "off",
|
||||
});
|
||||
|
||||
/** Vorschau (Zug + Gummiband zum Cursor) + HUD (Länge·Winkel des letzten Segments). */
|
||||
function polyDraft(points: Vec2[], cursor: Vec2 | null): ToolDraft {
|
||||
function polyDraft(points: Vec2[], cursor: Vec2 | null, closed = false): ToolDraft {
|
||||
const pts = cursor ? [...points, cursor] : points;
|
||||
const preview: DraftShape[] = [{ kind: "poly", pts, closed: false }];
|
||||
// Ring nur ab 3 Punkten zeigen; darunter bleibt die Vorschau offen.
|
||||
const showClosed = closed && pts.length >= 3;
|
||||
const preview: DraftShape[] = [{ kind: "poly", pts, closed: showClosed }];
|
||||
const draft: ToolDraft = { preview, vertices: points };
|
||||
const last = points[points.length - 1];
|
||||
if (cursor && last && segLen(last, cursor) >= EPS) {
|
||||
@@ -89,8 +102,9 @@ function appendPolyline(
|
||||
return { ...p, drawings2d: [...p.drawings2d, d] };
|
||||
}
|
||||
|
||||
const idle = (): [CommandState, CommandResult] => [
|
||||
{ phase: "start", lastPoint: null },
|
||||
/** Ruhezustand; behält den zuletzt gewählten Schließ-Modus bei. */
|
||||
const idle = (closed = false): [CommandState, CommandResult] => [
|
||||
{ phase: "start", lastPoint: null, closed } as PolyIdle,
|
||||
{ draft: null, done: true },
|
||||
];
|
||||
|
||||
@@ -98,77 +112,95 @@ export const polylineCommand: Command = {
|
||||
name: "polyline",
|
||||
labelKey: "cmd.polyline.label",
|
||||
prompt: (s) => ((s as PolyState).phase === "next" ? "cmd.polyline.next" : "cmd.polyline.start"),
|
||||
accepts: (s) =>
|
||||
(s as PolyState).phase === "next" ? ["point", "number", "option"] : ["point", "number"],
|
||||
// Auch im Startschritt darf der Schließ-Modus gewählt werden.
|
||||
accepts: () => ["point", "number", "option"],
|
||||
options: (s) => {
|
||||
const ps = s as PolyState;
|
||||
if (ps.phase !== "next") return [];
|
||||
return ps.points.length >= 2 ? [CLOSE, UNDO] : [UNDO];
|
||||
const toggle = closedOption(ps.closed);
|
||||
if (ps.phase !== "next") return [toggle];
|
||||
return ps.points.length >= 2 ? [CLOSE, toggle, UNDO] : [toggle, UNDO];
|
||||
},
|
||||
init: (): PolyIdle => ({ phase: "start", lastPoint: null }),
|
||||
init: (): PolyIdle => ({ phase: "start", lastPoint: null, closed: false }),
|
||||
|
||||
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
||||
const s = state as PolyState;
|
||||
|
||||
if (input.kind === "option") {
|
||||
// Schließ-Modus umschalten (Tastenkürzel „C" bzw. Klick im Feld-Row);
|
||||
// in jedem Schritt verfügbar, ohne den laufenden Zug zu beenden.
|
||||
if (input.id === "mode") {
|
||||
const ns = { ...s, closed: !s.closed } as PolyState;
|
||||
return [ns, { draft: s.phase === "next" ? polyDraft(s.points, s.cursor, ns.closed) : null }];
|
||||
}
|
||||
if (s.phase !== "next") return [s, { draft: null }];
|
||||
if (input.id === "undo") {
|
||||
const pts = s.points.slice(0, -1);
|
||||
if (pts.length === 0) return [{ phase: "start", lastPoint: null }, { draft: null }];
|
||||
const ns: PolyDrawing = { phase: "next", points: pts, cursor: s.cursor, lastPoint: pts[pts.length - 1] };
|
||||
return [ns, { draft: polyDraft(pts, s.cursor) }];
|
||||
if (pts.length === 0)
|
||||
return [{ phase: "start", lastPoint: null, closed: s.closed } as PolyIdle, { draft: null }];
|
||||
const ns: PolyDrawing = {
|
||||
phase: "next",
|
||||
points: pts,
|
||||
cursor: s.cursor,
|
||||
lastPoint: pts[pts.length - 1],
|
||||
closed: s.closed,
|
||||
};
|
||||
return [ns, { draft: polyDraft(pts, s.cursor, s.closed) }];
|
||||
}
|
||||
// Sofort schließen: committet als Ring (unabhängig vom Toggle-Zustand).
|
||||
if (input.id === "close" && s.points.length >= 3) {
|
||||
const pts = s.points;
|
||||
return [
|
||||
{ phase: "start", lastPoint: null },
|
||||
{ phase: "start", lastPoint: null, closed: s.closed } as PolyIdle,
|
||||
{ draft: null, done: true, commit: (p) => appendPolyline(p, pts, true, ctx) },
|
||||
];
|
||||
}
|
||||
return [s, { draft: polyDraft(s.points, s.cursor) }];
|
||||
return [s, { draft: polyDraft(s.points, s.cursor, s.closed) }];
|
||||
}
|
||||
|
||||
if (input.kind !== "point") {
|
||||
return [s, { draft: s.phase === "next" ? polyDraft(s.points, s.cursor) : null }];
|
||||
return [s, { draft: s.phase === "next" ? polyDraft(s.points, s.cursor, s.closed) : null }];
|
||||
}
|
||||
const pt = input.point;
|
||||
if (s.phase !== "next") {
|
||||
const ns: PolyDrawing = { phase: "next", points: [pt], cursor: pt, lastPoint: pt };
|
||||
return [ns, { draft: polyDraft([pt], pt) }];
|
||||
const ns: PolyDrawing = { phase: "next", points: [pt], cursor: pt, lastPoint: pt, closed: s.closed };
|
||||
return [ns, { draft: polyDraft([pt], pt, s.closed) }];
|
||||
}
|
||||
// Klick nahe Startpunkt → schließen.
|
||||
// Klick nahe Startpunkt → schließen (committet als Ring).
|
||||
if (s.points.length >= 3 && segLen(s.points[0], pt) < CLOSE_HIT) {
|
||||
const pts = s.points;
|
||||
return [
|
||||
{ phase: "start", lastPoint: null },
|
||||
{ phase: "start", lastPoint: null, closed: s.closed } as PolyIdle,
|
||||
{ draft: null, done: true, commit: (p) => appendPolyline(p, pts, true, ctx) },
|
||||
];
|
||||
}
|
||||
const points = [...s.points, pt];
|
||||
const ns: PolyDrawing = { phase: "next", points, cursor: pt, lastPoint: pt };
|
||||
return [ns, { draft: polyDraft(points, pt) }];
|
||||
const ns: PolyDrawing = { phase: "next", points, cursor: pt, lastPoint: pt, closed: s.closed };
|
||||
return [ns, { draft: polyDraft(points, pt, s.closed) }];
|
||||
},
|
||||
|
||||
onMove: (state, point): [CommandState, CommandResult] => {
|
||||
const s = state as PolyState;
|
||||
if (s.phase !== "next") return [s, { draft: null }];
|
||||
const ns: PolyDrawing = { ...s, cursor: point };
|
||||
return [ns, { draft: polyDraft(s.points, point) }];
|
||||
return [ns, { draft: polyDraft(s.points, point, s.closed) }];
|
||||
},
|
||||
|
||||
// Enter/Space/Rechtsklick: offenen Zug beenden (≥2 Punkte) und committen.
|
||||
// Enter/Space/Rechtsklick: Zug beenden (≥2 Punkte). Der Schließ-Modus des
|
||||
// Toggles bestimmt, ob als Ring oder offen committet wird. Ein Ring braucht
|
||||
// ≥3 Punkte, sonst fällt er auf „offen" zurück.
|
||||
onConfirm: (state, ctx): [CommandState, CommandResult] => {
|
||||
const s = state as PolyState;
|
||||
if (s.phase === "next" && s.points.length >= 2) {
|
||||
const pts = s.points;
|
||||
const close = s.closed && pts.length >= 3;
|
||||
return [
|
||||
{ phase: "start", lastPoint: null },
|
||||
{ draft: null, done: true, commit: (p) => appendPolyline(p, pts, false, ctx) },
|
||||
{ phase: "start", lastPoint: null, closed: s.closed } as PolyIdle,
|
||||
{ draft: null, done: true, commit: (p) => appendPolyline(p, pts, close, ctx) },
|
||||
];
|
||||
}
|
||||
return idle();
|
||||
return idle(s.closed);
|
||||
},
|
||||
onCancel: (): [CommandState, CommandResult] => idle(),
|
||||
onCancel: (state): [CommandState, CommandResult] => idle((state as PolyState).closed),
|
||||
|
||||
// Tab-Feld-Zyklus nur im „next"-Schritt: Länge + Winkel relativ zum letzten
|
||||
// Punkt. (Erster Punkt = freie Koordinate, kein Feld-Modus.)
|
||||
@@ -178,4 +210,13 @@ export const polylineCommand: Command = {
|
||||
if (s.phase !== "next" || s.points.length === 0) return null;
|
||||
return polyNextFromFields(s.points[s.points.length - 1], locks, cursor);
|
||||
},
|
||||
fieldValues: (state, _locks, cursor): Record<string, number> => {
|
||||
const s = state as PolyState;
|
||||
if (s.phase !== "next" || s.points.length === 0 || !cursor) return {};
|
||||
const last = s.points[s.points.length - 1];
|
||||
return {
|
||||
length: segLen(last, cursor),
|
||||
angle: ((segAngleDeg(last, cursor) % 360) + 360) % 360,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user