Open technical graphics as four years of AutoCAD.

Each lesson has a method, a dimensioned assignment and a quiz, from the drawing sheet through the exam model.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-10-11 13:44:38 +02:00
commit 6662eb185e
84 changed files with 3355 additions and 6 deletions

View file

@ -234,6 +234,26 @@ final class AppModel: ObservableObject {
refresh()
}
func markTg(_ id: Int) {
progress.markTg(id)
refresh()
}
func markTg2(_ id: Int) {
progress.markTg2(id)
refresh()
}
func markTg3(_ id: Int) {
progress.markTg3(id)
refresh()
}
func markTg4(_ id: Int) {
progress.markTg4(id)
refresh()
}
func markMat0(_ id: Int) {
progress.markMat0(id)
refresh()

View file

@ -59,6 +59,10 @@ final class Content {
let fyz2: [J]
let fyz3: [J]
let fyz4: [J]
let tg: [J]
let tg2: [J]
let tg3: [J]
let tg4: [J]
let mat0: [J]
let mat: [J]
let mat2: [J]
@ -125,6 +129,10 @@ final class Content {
fyz2 = raw.arr("fyz2")
fyz3 = raw.arr("fyz3")
fyz4 = raw.arr("fyz4")
tg = raw.arr("tg")
tg2 = raw.arr("tg2")
tg3 = raw.arr("tg3")
tg4 = raw.arr("tg4")
mat0 = raw.arr("mat0")
mat = raw.arr("mat")
mat2 = raw.arr("mat2")
@ -187,6 +195,10 @@ final class Content {
func fyz2Lesson(_ id: Int) -> J? { fyz2.first { $0.int("id") == id } }
func fyz3Lesson(_ id: Int) -> J? { fyz3.first { $0.int("id") == id } }
func fyz4Lesson(_ id: Int) -> J? { fyz4.first { $0.int("id") == id } }
func tgLesson(_ id: Int) -> J? { tg.first { $0.int("id") == id } }
func tg2Lesson(_ id: Int) -> J? { tg2.first { $0.int("id") == id } }
func tg3Lesson(_ id: Int) -> J? { tg3.first { $0.int("id") == id } }
func tg4Lesson(_ id: Int) -> J? { tg4.first { $0.int("id") == id } }
func mat0Lesson(_ id: Int) -> J? { mat0.first { $0.int("id") == id } }
func matLesson(_ id: Int) -> J? { mat.first { $0.int("id") == id } }
func mat2Lesson(_ id: Int) -> J? { mat2.first { $0.int("id") == id } }

View file

@ -94,6 +94,14 @@ final class ProgressStore {
func markFyz3(_ id: Int) { mark("fyz3.\(id)") }
func fyz4Done(_ id: Int) -> Bool { done("fyz4.\(id)") }
func markFyz4(_ id: Int) { mark("fyz4.\(id)") }
func tgDone(_ id: Int) -> Bool { done("tg.\(id)") }
func markTg(_ id: Int) { mark("tg.\(id)") }
func tg2Done(_ id: Int) -> Bool { done("tg2.\(id)") }
func markTg2(_ id: Int) { mark("tg2.\(id)") }
func tg3Done(_ id: Int) -> Bool { done("tg3.\(id)") }
func markTg3(_ id: Int) { mark("tg3.\(id)") }
func tg4Done(_ id: Int) -> Bool { done("tg4.\(id)") }
func markTg4(_ id: Int) { mark("tg4.\(id)") }
func mat0Done(_ id: Int) -> Bool { done("mat0.\(id)") }
func markMat0(_ id: Int) { mark("mat0.\(id)") }
func matDone(_ id: Int) -> Bool { done("mat.\(id)") }
@ -288,6 +296,38 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
sum.doneUnits += 1
}
}
for l in content.tg {
sum.totalEx += 1
sum.openUnits += 1
if p.tgDone(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.tg2 {
sum.totalEx += 1
sum.openUnits += 1
if p.tg2Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.tg3 {
sum.totalEx += 1
sum.openUnits += 1
if p.tg3Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.tg4 {
sum.totalEx += 1
sum.openUnits += 1
if p.tg4Done(l.int("id")) {
sum.doneEx += 1
sum.doneUnits += 1
}
}
for l in content.mat0 {
sum.totalEx += 1
sum.openUnits += 1

View file

@ -111,6 +111,19 @@ enum Route: Hashable {
case fyz4Map
case fyz4Lesson(Int)
case fyz4Ex(Int)
case tgYears
case tgMap
case tgLesson(Int)
case tgEx(Int)
case tg2Map
case tg2Lesson(Int)
case tg2Ex(Int)
case tg3Map
case tg3Lesson(Int)
case tg3Ex(Int)
case tg4Map
case tg4Lesson(Int)
case tg4Ex(Int)
case czechMap
case mluvnice
case mluvEx(Int)
@ -224,6 +237,19 @@ enum Route: Hashable {
case .fyz4Map: return "fyz4map"
case .fyz4Lesson(let id): return "fyz4unit\(id)"
case .fyz4Ex(let id): return "fyz4ex\(id)"
case .tgYears: return "tgyears"
case .tgMap: return "tgmap"
case .tgLesson(let id): return "tgunit\(id)"
case .tgEx(let id): return "tgex\(id)"
case .tg2Map: return "tg2map"
case .tg2Lesson(let id): return "tg2unit\(id)"
case .tg2Ex(let id): return "tg2ex\(id)"
case .tg3Map: return "tg3map"
case .tg3Lesson(let id): return "tg3unit\(id)"
case .tg3Ex(let id): return "tg3ex\(id)"
case .tg4Map: return "tg4map"
case .tg4Lesson(let id): return "tg4unit\(id)"
case .tg4Ex(let id): return "tg4ex\(id)"
case .czechMap: return "czechmap"
case .mluvnice: return "mluvnice"
case .mluvEx(let n): return "mluve\(n)"
@ -448,6 +474,32 @@ enum Route: Hashable {
return [.fyzYears, .fyz4Map, .fyz4Lesson(id)]
case .fyz4Ex(let id):
return [.fyzYears, .fyz4Map, .fyz4Lesson(id), .fyz4Ex(id)]
case .tgYears:
return [.tgYears]
case .tgMap:
return [.tgYears, .tgMap]
case .tgLesson(let id):
return [.tgYears, .tgMap, .tgLesson(id)]
case .tgEx(let id):
return [.tgYears, .tgMap, .tgLesson(id), .tgEx(id)]
case .tg2Map:
return [.tgYears, .tg2Map]
case .tg2Lesson(let id):
return [.tgYears, .tg2Map, .tg2Lesson(id)]
case .tg2Ex(let id):
return [.tgYears, .tg2Map, .tg2Lesson(id), .tg2Ex(id)]
case .tg3Map:
return [.tgYears, .tg3Map]
case .tg3Lesson(let id):
return [.tgYears, .tg3Map, .tg3Lesson(id)]
case .tg3Ex(let id):
return [.tgYears, .tg3Map, .tg3Lesson(id), .tg3Ex(id)]
case .tg4Map:
return [.tgYears, .tg4Map]
case .tg4Lesson(let id):
return [.tgYears, .tg4Map, .tg4Lesson(id)]
case .tg4Ex(let id):
return [.tgYears, .tg4Map, .tg4Lesson(id), .tg4Ex(id)]
case .czechMap:
return [.czechMap]
case .mluvnice:
@ -508,6 +560,11 @@ func routeFromPage(_ page: String) -> Route? {
case "fyz2map": return .fyz2Map
case "fyz3map": return .fyz3Map
case "fyz4map": return .fyz4Map
case "tgyears": return .tgYears
case "tgmap": return .tgMap
case "tg2map": return .tg2Map
case "tg3map": return .tg3Map
case "tg4map": return .tg4Map
case "czechmap": return .czechMap
case "mluvnice": return .mluvnice
case "readinglist": return .readingList
@ -617,6 +674,30 @@ func routeFromPage(_ page: String) -> Route? {
if let m = page.wholeMatch(of: /fyzex(\d+)/), let n = Int(m.1) {
return .fyzEx(n)
}
if let m = page.wholeMatch(of: /tg4unit(\d+)/), let n = Int(m.1) {
return .tg4Lesson(n)
}
if let m = page.wholeMatch(of: /tg4ex(\d+)/), let n = Int(m.1) {
return .tg4Ex(n)
}
if let m = page.wholeMatch(of: /tg3unit(\d+)/), let n = Int(m.1) {
return .tg3Lesson(n)
}
if let m = page.wholeMatch(of: /tg3ex(\d+)/), let n = Int(m.1) {
return .tg3Ex(n)
}
if let m = page.wholeMatch(of: /tg2unit(\d+)/), let n = Int(m.1) {
return .tg2Lesson(n)
}
if let m = page.wholeMatch(of: /tg2ex(\d+)/), let n = Int(m.1) {
return .tg2Ex(n)
}
if let m = page.wholeMatch(of: /tgunit(\d+)/), let n = Int(m.1) {
return .tgLesson(n)
}
if let m = page.wholeMatch(of: /tgex(\d+)/), let n = Int(m.1) {
return .tgEx(n)
}
if let m = page.wholeMatch(of: /lit4unit(\d+)/), let n = Int(m.1) {
return .lit4Lesson(n)
}

View file

@ -679,6 +679,7 @@ func subjectSymbol(_ icon: String) -> String {
case "flask": return "flask.fill"
case "bolt": return "bolt.fill"
case "function": return "function"
case "ruler": return "ruler.fill"
default: return "lock.fill"
}
}

View file

@ -952,6 +952,39 @@ struct ChoiceLessonScreen: View {
}
}
struct TgQuizScreen: View {
@ObservedObject var vm: AppModel
let year: Int
let id: Int
var body: some View {
let lesson: J? = {
switch year {
case 2: return vm.content.tg2Lesson(id)
case 3: return vm.content.tg3Lesson(id)
case 4: return vm.content.tg4Lesson(id)
default: return vm.content.tgLesson(id)
}
}()
if let lesson {
let spec = J([
"type": "choice",
"title": vm.tr(lesson.str("exTitleKey")),
"sub": lesson.str("quizHeadKey"),
"questions": lesson.rawArray("quiz"),
])
DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
switch year {
case 2: vm.markTg2(id)
case 3: vm.markTg3(id)
case 4: vm.markTg4(id)
default: vm.markTg(id)
}
}
}
}
}
struct FyzQuizScreen: View {
@ObservedObject var vm: AppModel
let year: Int

View file

@ -713,6 +713,12 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
+ vm.content.mat2.filter { vm.progress.mat2Done($0.int("id")) }.count
+ vm.content.mat3.filter { vm.progress.mat3Done($0.int("id")) }.count
+ vm.content.mat4.filter { vm.progress.mat4Done($0.int("id")) }.count
case "tgyears", "tgmap", "tg2map", "tg3map", "tg4map":
sum.totalEx = vm.content.tg.count + vm.content.tg2.count + vm.content.tg3.count + vm.content.tg4.count
sum.doneEx = vm.content.tg.filter { vm.progress.tgDone($0.int("id")) }.count
+ vm.content.tg2.filter { vm.progress.tg2Done($0.int("id")) }.count
+ vm.content.tg3.filter { vm.progress.tg3Done($0.int("id")) }.count
+ vm.content.tg4.filter { vm.progress.tg4Done($0.int("id")) }.count
case "fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map":
sum.totalEx = vm.content.fyz.count + vm.content.fyz2.count + vm.content.fyz3.count + vm.content.fyz4.count
sum.doneEx = vm.content.fyz.filter { vm.progress.fyzDone($0.int("id")) }.count
@ -1015,6 +1021,27 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "fyz4unit\(l.int("id"))", extra: "fyzika")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "fyz4ex\(l.int("id"))")
}
add(vm.tr("Technická grafika"), vm.tr("search_lesson"), "tgyears", extra: "autocad grafika")
add(vm.tr("tg_year1"), vm.tr("search_lesson"), "tgmap", extra: "rocnik")
add(vm.tr("tg_year2"), vm.tr("search_lesson"), "tg2map", extra: "rocnik")
add(vm.tr("tg_year3"), vm.tr("search_lesson"), "tg3map", extra: "rocnik")
add(vm.tr("tg_year4"), vm.tr("search_lesson"), "tg4map", extra: "rocnik")
for l in vm.content.tg {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "tgunit\(l.int("id"))", extra: "autocad")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tgex\(l.int("id"))")
}
for l in vm.content.tg2 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "tg2unit\(l.int("id"))", extra: "autocad")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tg2ex\(l.int("id"))")
}
for l in vm.content.tg3 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "tg3unit\(l.int("id"))", extra: "autocad")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tg3ex\(l.int("id"))")
}
for l in vm.content.tg4 {
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "tg4unit\(l.int("id"))", extra: "autocad")
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tg4ex\(l.int("id"))")
}
add(vm.tr("Chemie"), vm.tr("search_lesson"), "chemmap", extra: "chemie")
add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra: "biologie")
for l in vm.content.chem {
@ -1749,6 +1776,47 @@ struct MatYearsScreen: View {
}
}
struct TgYearsScreen: View {
@ObservedObject var vm: AppModel
var body: some View {
#if os(macOS)
let years: [(String, String, Route)] = [
("tg_year1", "tg_sub", .tgMap),
("tg_year2", "tg2_sub", .tg2Map),
("tg_year3", "tg3_sub", .tg3Map),
("tg_year4", "tg4_sub", .tg4Map),
]
MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in
NavigationLink(value: years[i].2) {
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
}
.buttonStyle(PressScaleStyle())
}
.navigationTitle(vm.tr("Technická grafika"))
.navigationSubtitle(vm.tr("tg_years_sub"))
#else
List {
NavigationLink(value: Route.tgMap) {
ListRowLabel(title: vm.tr("tg_year1"), subtitle: vm.tr("tg_sub"))
}
NavigationLink(value: Route.tg2Map) {
ListRowLabel(title: vm.tr("tg_year2"), subtitle: vm.tr("tg2_sub"))
}
NavigationLink(value: Route.tg3Map) {
ListRowLabel(title: vm.tr("tg_year3"), subtitle: vm.tr("tg3_sub"))
}
NavigationLink(value: Route.tg4Map) {
ListRowLabel(title: vm.tr("tg_year4"), subtitle: vm.tr("tg4_sub"))
}
}
.navigationTitle(vm.tr("Technická grafika"))
.navigationSubtitle(vm.tr("tg_years_sub"))
.appListStyle()
#endif
}
}
struct FyzYearsScreen: View {
@ObservedObject var vm: AppModel
@ -2808,6 +2876,72 @@ struct RouteDestination: View {
}
case .fyz4Ex(let id):
FyzQuizScreen(vm: vm, year: 4, id: id)
case .tgYears:
TgYearsScreen(vm: vm)
case .tgMap:
LessonListScreen(
vm: vm,
title: vm.tr("tg_year1"),
subtitle: vm.tr("tg_sub"),
rows: vm.content.tg.map { l in
let id = l.int("id")
return LessonRow(id: "t\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.tgDone(id), destination: .tgLesson(id))
}
)
case .tgLesson(let id):
if let l = vm.content.tgLesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .tgEx(id))
}
case .tgEx(let id):
TgQuizScreen(vm: vm, year: 1, id: id)
case .tg2Map:
LessonListScreen(
vm: vm,
title: vm.tr("tg_year2"),
subtitle: vm.tr("tg2_sub"),
rows: vm.content.tg2.map { l in
let id = l.int("id")
return LessonRow(id: "t2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.tg2Done(id), destination: .tg2Lesson(id))
}
)
case .tg2Lesson(let id):
if let l = vm.content.tg2Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .tg2Ex(id))
}
case .tg2Ex(let id):
TgQuizScreen(vm: vm, year: 2, id: id)
case .tg3Map:
LessonListScreen(
vm: vm,
title: vm.tr("tg_year3"),
subtitle: vm.tr("tg3_sub"),
rows: vm.content.tg3.map { l in
let id = l.int("id")
return LessonRow(id: "t3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.tg3Done(id), destination: .tg3Lesson(id))
}
)
case .tg3Lesson(let id):
if let l = vm.content.tg3Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .tg3Ex(id))
}
case .tg3Ex(let id):
TgQuizScreen(vm: vm, year: 3, id: id)
case .tg4Map:
LessonListScreen(
vm: vm,
title: vm.tr("tg_year4"),
subtitle: vm.tr("tg4_sub"),
rows: vm.content.tg4.map { l in
let id = l.int("id")
return LessonRow(id: "t4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.tg4Done(id), destination: .tg4Lesson(id))
}
)
case .tg4Lesson(let id):
if let l = vm.content.tg4Lesson(id) {
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .tg4Ex(id))
}
case .tg4Ex(let id):
TgQuizScreen(vm: vm, year: 4, id: id)
case .mluvnice:
LessonListScreen(
vm: vm,