Befehl 'Deckenloch': Aussparung als Rechteck in eine Decke zeichnen
Zwei Klicks (Ecke → Gegenecke, Live-Vorschau + Mass-HUD); die Aussparung wird
der Decke des aktiven Geschosses hinzugefügt, die das Rechteck vollständig
enthält (alle 4 Ecken im Umriss), sonst No-op. Aliase 'deckenloch'/
'aussparung', BIM-Ribbon-Eintrag mit eigenem Icon. Komplettiert die Decken-
Aussparungen (cdbef99) um den Zeichenweg. 684/684 grün.
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
// Deckenloch — Aussparung (Treppenauge/Schacht) als Engine-Befehl: ein RECHTECK
|
||||||
|
// wird aufgezogen (zwei Klicks: erste Ecke → Gegenecke) und der Decke des
|
||||||
|
// aktiven Geschosses hinzugefügt, die das Rechteck ENTHÄLT (alle vier Ecken
|
||||||
|
// innerhalb des Decken-Umrisses). Gerendert wird das Loch in 2D (Poché-Loch +
|
||||||
|
// Randlinien) und 3D (Loch im Slab), s. Ceiling.openings/mergeHoles.
|
||||||
|
//
|
||||||
|
// Schritte:
|
||||||
|
// 1) „Erste Ecke der Aussparung:" → Punkt
|
||||||
|
// 2) „Gegenecke:" → Live-Rechteck-Vorschau; Klick committet.
|
||||||
|
|
||||||
|
import { pointInOutline } from "../../geometry/ceiling";
|
||||||
|
import type {
|
||||||
|
Command,
|
||||||
|
CommandContext,
|
||||||
|
CommandResult,
|
||||||
|
CommandState,
|
||||||
|
DraftShape,
|
||||||
|
Project,
|
||||||
|
ToolDraft,
|
||||||
|
Vec2,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
/** Vier Eckpunkte eines Rechtecks aus zwei Gegenecken (CCW). */
|
||||||
|
function rectCorners(a: Vec2, b: Vec2): Vec2[] {
|
||||||
|
const x0 = Math.min(a.x, b.x);
|
||||||
|
const x1 = Math.max(a.x, b.x);
|
||||||
|
const y0 = Math.min(a.y, b.y);
|
||||||
|
const y1 = Math.max(a.y, b.y);
|
||||||
|
return [
|
||||||
|
{ x: x0, y: y0 },
|
||||||
|
{ x: x1, y: y0 },
|
||||||
|
{ x: x1, y: y1 },
|
||||||
|
{ x: x0, y: y1 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HoleIdle extends CommandState {
|
||||||
|
phase: "start";
|
||||||
|
}
|
||||||
|
interface HoleRect extends CommandState {
|
||||||
|
phase: "corner";
|
||||||
|
a: Vec2;
|
||||||
|
cursor: Vec2 | null;
|
||||||
|
}
|
||||||
|
type HoleState = HoleIdle | HoleRect;
|
||||||
|
|
||||||
|
/** Vorschau: aufgezogenes Rechteck + Masse im HUD. */
|
||||||
|
function holeDraft(a: Vec2, cursor: Vec2 | null): ToolDraft {
|
||||||
|
const pts = cursor ? rectCorners(a, cursor) : [a];
|
||||||
|
const preview: DraftShape[] = [{ kind: "poly", pts, closed: !!cursor }];
|
||||||
|
const draft: ToolDraft = { preview, vertices: [a] };
|
||||||
|
if (cursor) {
|
||||||
|
draft.hud = {
|
||||||
|
at: cursor,
|
||||||
|
text: `${Math.abs(cursor.x - a.x).toFixed(2)} × ${Math.abs(cursor.y - a.y).toFixed(2)} m`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt die Aussparung der ERSTEN Decke des aktiven Geschosses hinzu, die das
|
||||||
|
* Rechteck vollständig enthält (alle 4 Ecken im Umriss). Entartete Rechtecke
|
||||||
|
* und Rechtecke ausserhalb jeder Decke werden verworfen (Projekt unverändert).
|
||||||
|
*/
|
||||||
|
function appendCeilingOpening(p: Project, a: Vec2, b: Vec2, ctx: CommandContext): Project {
|
||||||
|
const w = Math.abs(b.x - a.x);
|
||||||
|
const d = Math.abs(b.y - a.y);
|
||||||
|
if (w < 0.05 || d < 0.05) return p;
|
||||||
|
const corners = rectCorners(a, b);
|
||||||
|
const host = (p.ceilings ?? []).find(
|
||||||
|
(c) =>
|
||||||
|
c.floorId === ctx.level.id &&
|
||||||
|
c.outline.length >= 3 &&
|
||||||
|
corners.every((pt) => pointInOutline(pt, c.outline)),
|
||||||
|
);
|
||||||
|
if (!host) return p;
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
ceilings: (p.ceilings ?? []).map((c) =>
|
||||||
|
c.id === host.id ? { ...c, openings: [...(c.openings ?? []), corners] } : c,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const idle = (): [CommandState, CommandResult] => [
|
||||||
|
{ phase: "start", lastPoint: null } as HoleIdle,
|
||||||
|
{ draft: null, done: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ceilingOpeningCommand: Command = {
|
||||||
|
name: "ceilingopening",
|
||||||
|
labelKey: "cmd.ceilingOpening.label",
|
||||||
|
floorOnly: true,
|
||||||
|
prompt: (s) =>
|
||||||
|
(s as HoleState).phase === "corner" ? "cmd.ceilingOpening.corner" : "cmd.ceilingOpening.start",
|
||||||
|
accepts: () => ["point"],
|
||||||
|
options: () => [],
|
||||||
|
init: (): HoleIdle => ({ phase: "start", lastPoint: null }),
|
||||||
|
|
||||||
|
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
||||||
|
const s = state as HoleState;
|
||||||
|
if (input.kind !== "point") {
|
||||||
|
return [s, { draft: s.phase === "corner" ? holeDraft(s.a, s.cursor) : null }];
|
||||||
|
}
|
||||||
|
if (s.phase !== "corner") {
|
||||||
|
const ns: HoleRect = {
|
||||||
|
phase: "corner",
|
||||||
|
a: input.point,
|
||||||
|
cursor: input.point,
|
||||||
|
lastPoint: input.point,
|
||||||
|
};
|
||||||
|
return [ns, { draft: holeDraft(input.point, input.point) }];
|
||||||
|
}
|
||||||
|
// Zweite Ecke → committen.
|
||||||
|
const a = s.a;
|
||||||
|
const b = input.point;
|
||||||
|
return [
|
||||||
|
{ phase: "start", lastPoint: null } as HoleIdle,
|
||||||
|
{ draft: null, done: true, commit: (p) => appendCeilingOpening(p, a, b, ctx) },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
onMove: (state, point): [CommandState, CommandResult] => {
|
||||||
|
const s = state as HoleState;
|
||||||
|
if (s.phase !== "corner") return [s, { draft: null }];
|
||||||
|
const ns: HoleRect = { ...s, cursor: point };
|
||||||
|
return [ns, { draft: holeDraft(s.a, point) }];
|
||||||
|
},
|
||||||
|
|
||||||
|
onConfirm: (): [CommandState, CommandResult] => idle(),
|
||||||
|
onCancel: (): [CommandState, CommandResult] => idle(),
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ import { openingCommand, fensterCommand, tuerCommand } from "./cmds/opening";
|
|||||||
import { stairCommand } from "./cmds/stair";
|
import { stairCommand } from "./cmds/stair";
|
||||||
import { columnCommand } from "./cmds/column";
|
import { columnCommand } from "./cmds/column";
|
||||||
import { roofCommand } from "./cmds/roof";
|
import { roofCommand } from "./cmds/roof";
|
||||||
|
import { ceilingOpeningCommand } from "./cmds/ceilingOpening";
|
||||||
import { roomCommand } from "./cmds/room";
|
import { roomCommand } from "./cmds/room";
|
||||||
import { rectCommand } from "./cmds/rect";
|
import { rectCommand } from "./cmds/rect";
|
||||||
import { circleCommand } from "./cmds/circle";
|
import { circleCommand } from "./cmds/circle";
|
||||||
@@ -39,6 +40,7 @@ export const COMMANDS: Record<string, Command> = {
|
|||||||
stair: stairCommand,
|
stair: stairCommand,
|
||||||
column: columnCommand,
|
column: columnCommand,
|
||||||
roof: roofCommand,
|
roof: roofCommand,
|
||||||
|
ceilingopening: ceilingOpeningCommand,
|
||||||
room: roomCommand,
|
room: roomCommand,
|
||||||
line: lineCommand,
|
line: lineCommand,
|
||||||
polyline: polylineCommand,
|
polyline: polylineCommand,
|
||||||
@@ -78,6 +80,8 @@ export const ALIASES: Record<string, string> = {
|
|||||||
stuetze: "column",
|
stuetze: "column",
|
||||||
stütze: "column",
|
stütze: "column",
|
||||||
dach: "roof",
|
dach: "roof",
|
||||||
|
deckenloch: "ceilingopening",
|
||||||
|
aussparung: "ceilingopening",
|
||||||
rf: "roof",
|
rf: "roof",
|
||||||
raum: "room",
|
raum: "room",
|
||||||
rm: "room",
|
rm: "room",
|
||||||
|
|||||||
@@ -963,6 +963,9 @@ export const de = {
|
|||||||
"cmd.roof.label": "Dach",
|
"cmd.roof.label": "Dach",
|
||||||
"cmd.roof.start": "Erste Dachecke:",
|
"cmd.roof.start": "Erste Dachecke:",
|
||||||
"cmd.roof.corner": "Gegenecke:",
|
"cmd.roof.corner": "Gegenecke:",
|
||||||
|
"cmd.ceilingOpening.label": "Deckenloch",
|
||||||
|
"cmd.ceilingOpening.start": "Erste Ecke der Aussparung (in einer Decke):",
|
||||||
|
"cmd.ceilingOpening.corner": "Gegenecke:",
|
||||||
"cmd.roof.shape.sattel": "Satteldach",
|
"cmd.roof.shape.sattel": "Satteldach",
|
||||||
"cmd.roof.shape.walm": "Walmdach",
|
"cmd.roof.shape.walm": "Walmdach",
|
||||||
"cmd.roof.shape.pult": "Pultdach",
|
"cmd.roof.shape.pult": "Pultdach",
|
||||||
|
|||||||
@@ -953,6 +953,9 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"cmd.roof.label": "Roof",
|
"cmd.roof.label": "Roof",
|
||||||
"cmd.roof.start": "First roof corner:",
|
"cmd.roof.start": "First roof corner:",
|
||||||
"cmd.roof.corner": "Opposite corner:",
|
"cmd.roof.corner": "Opposite corner:",
|
||||||
|
"cmd.ceilingOpening.label": "Slab opening",
|
||||||
|
"cmd.ceilingOpening.start": "First corner of the opening (inside a slab):",
|
||||||
|
"cmd.ceilingOpening.corner": "Opposite corner:",
|
||||||
"cmd.roof.shape.sattel": "Gable roof",
|
"cmd.roof.shape.sattel": "Gable roof",
|
||||||
"cmd.roof.shape.walm": "Hip roof",
|
"cmd.roof.shape.walm": "Hip roof",
|
||||||
"cmd.roof.shape.pult": "Mono-pitch roof",
|
"cmd.roof.shape.pult": "Mono-pitch roof",
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ export function CommandIcon({ name }: { name: string }) {
|
|||||||
<line x1="8" y1="3" x2="8" y2="9" strokeDasharray="1.5 1.5" />
|
<line x1="8" y1="3" x2="8" y2="9" strokeDasharray="1.5 1.5" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
case "ceilingopening":
|
||||||
|
// Deckenloch: Decken-Rechteck mit ausgespartem Innenrechteck.
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<rect x="2.5" y="3.5" width="11" height="9" />
|
||||||
|
<rect x="6" y="6.5" width="4" height="3" strokeDasharray="1.4 1.2" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export const RIBBON_TABS: RibbonTab[] = [
|
|||||||
t("stair"),
|
t("stair"),
|
||||||
c("column"),
|
c("column"),
|
||||||
t("ceiling"),
|
t("ceiling"),
|
||||||
|
c("ceilingopening"),
|
||||||
c("roof"),
|
c("roof"),
|
||||||
t("room"),
|
t("room"),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user